├── .gitignore ├── README.md ├── composer.json ├── composer.lock ├── examples ├── dead-code.php ├── print-cfg-ssa.php ├── print-cfg.php └── undefined-variables.php └── src ├── CFG ├── Analysis │ ├── DataFlow │ │ ├── BackwardAnalysis.php │ │ ├── DefUses.php │ │ ├── DefinitionGenKills.php │ │ ├── ForwardAnalysis.php │ │ ├── LiveVariablesAnalysis.php │ │ ├── ReachingDefinitionsAnalysis.php │ │ ├── UndefinedVariableAnalysis.php │ │ └── UndefinedVariableStatus.php │ ├── DeadCodeAnalysisSSA.php │ └── Dominance.php ├── BBlock.php ├── CFG.php ├── CFGBuilder.php ├── NodeContext.php ├── NodeUtils.php ├── SSA │ ├── Conversion │ │ ├── BlockDefUse.php │ │ ├── RenameProcess.php │ │ └── SSAConversion.php │ ├── Node │ │ └── Phi.php │ ├── SSASymTable.php │ └── SSAUtils.php └── SymTable.php ├── Utils ├── BitSet.php ├── FunctionUtils.php └── SideEffectVisitor.php └── Visualization ├── AST ├── StandardPrettyPrinter.php └── TerminatorPrettyPrinter.php └── CFG ├── Annotator ├── GraphvizDeadStmtAnnotator.php ├── GraphvizLineNumberStmtAnnotator.php ├── GraphvizStmtAnnotator.php └── GraphvizStmtClassAnnotator.php ├── GraphvizPrinter.php └── GraphvizPrinterBuilder.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # php-sema 2 | 3 | Semantic analysis of PHP code. 4 | 5 | php-sema implements an AST-based CFG that is suitable for source-level analysis. 6 | 7 | Currently, php-sema implements: SSA conversion, generic data flow analysis algorithms, reachable definitions, dead code analysis, undefined variable analysis. 8 | 9 | ## Use-cases 10 | 11 | - Detecting uses of undefined variables 12 | - Detecting dead code (code without effect) 13 | - Detecting unreachable code 14 | 15 | ## Work in progress 16 | 17 | This is a work in progress. There is not enough tests, no support for variable-variables or references, and the API needs improvements. 18 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aleblanc/php-sema", 3 | "description": "An AST-based CFG for semantic analysis of PHP programs", 4 | "minimum-stability": "stable", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Arnaud Le Blanc", 9 | "email": "arnaud.lb@gmail.com" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "PhpSema\\": "src" 15 | } 16 | }, 17 | "autoload-dev": { 18 | "psr-4": { 19 | "PhpSema\\Test\\": "test" 20 | } 21 | }, 22 | "require": { 23 | "php": ">= 8.1", 24 | "nikic/php-parser": "^4.13", 25 | "clue/graph": "^0.9.3", 26 | "graphp/graphviz": "^0.2.2" 27 | }, 28 | "require-dev": { 29 | "phpstan/phpstan": "^1.5", 30 | "symfony/console": "^6.0", 31 | "phpunit/phpunit": "^9.5", 32 | "symfony/finder": "^6.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/dead-code.php: -------------------------------------------------------------------------------- 1 | create(\PhpParser\ParserFactory::PREFER_PHP7); 16 | $ast = $parser->parse(file_get_contents($input->getArgument('filename'))); 17 | assert($ast !== null); 18 | 19 | $finder = new \PhpParser\NodeFinder(); 20 | $nodes = $finder->find($ast, function (\PhpParser\Node $node) use ($input) { 21 | return match (true) { 22 | $node instanceof \PhpParser\Node\FunctionLike => $input->getArgument('function') !== null 23 | ? $input->getArgument('function') === FunctionUtils::getName($node) 24 | : true, 25 | default => false, 26 | }; 27 | }); 28 | 29 | if (count($nodes) === 0) { 30 | fprintf(STDERR, "Found no functions or methods\n"); 31 | exit(1); 32 | } 33 | 34 | foreach ($nodes as $node) { 35 | assert($node instanceof \PhpParser\Node\FunctionLike); 36 | 37 | fprintf(STDERR, "Analyzing %s\n", FunctionUtils::getName($node) ?? 'anon function'); 38 | 39 | $start = microtime(true); 40 | 41 | $cfg = (new \PhpSema\CFG\CFGBuilder())->buildFunction($node); 42 | $ssaSymTable = \PhpSema\CFG\SSA\Conversion\SSAConversion::convert($cfg); 43 | $deadCodeAnalysis = new \PhpSema\CFG\Analysis\DeadCodeAnalysisSSA($cfg, $ssaSymTable); 44 | 45 | $end = microtime(true); 46 | 47 | fprintf(STDERR, "Analysis done (%.03fms). Displaying CFG\n", ($end-$start)*1000); 48 | 49 | \PhpSema\Visualization\CFG\GraphvizPrinterBuilder::create($cfg) 50 | ->withPreStmtAnnotator( 51 | 'Line', 52 | new \PhpSema\Visualization\CFG\Annotator\GraphvizLineNumberStmtAnnotator(), 53 | ) 54 | ->withPreStmtAnnotator( 55 | 'Dead', 56 | new \PhpSema\Visualization\CFG\Annotator\GraphvizDeadStmtAnnotator($deadCodeAnalysis), 57 | ) 58 | ->withPostStmtAnnotator( 59 | 'Class', new \PhpSema\Visualization\CFG\Annotator\GraphvizStmtClassAnnotator(), 60 | ) 61 | ->printer() 62 | ->display(); 63 | } -------------------------------------------------------------------------------- /examples/print-cfg-ssa.php: -------------------------------------------------------------------------------- 1 | create(\PhpParser\ParserFactory::PREFER_PHP7); 6 | $ast = $parser->parse(file_get_contents($argv[1])); 7 | assert($ast !== null); 8 | 9 | $cfg = (new \PhpSema\CFG\CFGBuilder())->build($ast); 10 | $ssaSymTable = \PhpSema\CFG\SSA\Conversion\SSAConversion::convert($cfg); 11 | 12 | \PhpSema\Visualization\CFG\GraphvizPrinterBuilder::create($cfg)->printer()->display(); -------------------------------------------------------------------------------- /examples/print-cfg.php: -------------------------------------------------------------------------------- 1 | create(\PhpParser\ParserFactory::PREFER_PHP7); 14 | $ast = $parser->parse(file_get_contents($input->getArgument('filename'))); 15 | assert($ast !== null); 16 | 17 | $cfg = (new \PhpSema\CFG\CFGBuilder())->build($ast); 18 | 19 | \PhpSema\Visualization\CFG\GraphvizPrinterBuilder::create($cfg) 20 | ->withPreStmtAnnotator( 21 | 'Line', 22 | new \PhpSema\Visualization\CFG\Annotator\GraphvizLineNumberStmtAnnotator(), 23 | ) 24 | ->withPostStmtAnnotator( 25 | 'Class', new \PhpSema\Visualization\CFG\Annotator\GraphvizStmtClassAnnotator(), 26 | ) 27 | ->printer() 28 | ->display(); 29 | -------------------------------------------------------------------------------- /examples/undefined-variables.php: -------------------------------------------------------------------------------- 1 | create(\PhpParser\ParserFactory::PREFER_PHP7); 14 | $ast = $parser->parse(file_get_contents($input->getArgument('filename'))); 15 | assert($ast !== null); 16 | 17 | $cfg = (new \PhpSema\CFG\CFGBuilder())->build($ast); 18 | 19 | $analysis = \PhpSema\CFG\Analysis\DataFlow\UndefinedVariableAnalysis::fromCFG($cfg); 20 | 21 | foreach ($cfg->getBBlocks() as $block) { 22 | foreach ($block->getStmts() as $stmt) { 23 | if (!$stmt instanceof \PhpParser\Node\Expr\Variable) { 24 | continue; 25 | } 26 | if (!is_string($stmt->name)) { 27 | continue; 28 | } 29 | switch ($analysis->getVariableStatus($stmt)) { 30 | case \PhpSema\CFG\Analysis\DataFlow\UndefinedVariableStatus::Defined: 31 | break; 32 | case \PhpSema\CFG\Analysis\DataFlow\UndefinedVariableStatus::Unkonwn: 33 | break; 34 | case \PhpSema\CFG\Analysis\DataFlow\UndefinedVariableStatus::MaybeUndefined: 35 | fprintf( 36 | STDERR, 37 | "Variable \$%s may be undefined when used at line %d\n", 38 | $stmt->name, 39 | $stmt->getLine(), 40 | ); 41 | break; 42 | case \PhpSema\CFG\Analysis\DataFlow\UndefinedVariableStatus::Undefined: 43 | fprintf( 44 | STDERR, 45 | "Variable \$%s is undefined when used at line %d\n", 46 | $stmt->name, 47 | $stmt->getLine(), 48 | ); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/CFG/Analysis/DataFlow/BackwardAnalysis.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class BackwardAnalysis 14 | { 15 | /** @var array */ 16 | private array $in; 17 | 18 | /** @var array */ 19 | private array $out; 20 | 21 | /** 22 | * @param callable(V,V):V $meet 23 | * @param callable(BBlock,V):V $transfer 24 | * @param callable(V,V):bool $equals 25 | * @param V $boundary 26 | * @param V $init 27 | */ 28 | public function __construct( 29 | CFG $cfg, 30 | $meet, 31 | $transfer, 32 | $equals, 33 | $boundary, 34 | $init, 35 | ) 36 | { 37 | $exit = $cfg->getExit(); 38 | $basicBlocks = $cfg->getBBlocks(); 39 | 40 | $in = array_fill(0, count($basicBlocks), $boundary); 41 | $in[$exit->getId()] = $init; 42 | 43 | $out = []; 44 | 45 | do { 46 | $changed = false; 47 | foreach ($basicBlocks as $id => $bb) { 48 | if ($bb === $exit) { 49 | continue; 50 | } 51 | 52 | $bbOut = $boundary; 53 | $bbIn = $in[$id]; 54 | 55 | foreach ($bb->getSuccessors() as $succ) { 56 | $bbOut = $meet($bbOut, $in[$succ->getId()]); 57 | } 58 | $out[$id] = $bbOut; 59 | $in[$id] = $transfer($bb, $bbOut); 60 | $changed = $changed || !$equals($bbIn, $in[$id]); 61 | } 62 | } while ($changed); 63 | 64 | $this->in = $in; 65 | $this->out = $out; 66 | } 67 | 68 | /** @return array */ 69 | public function getIn(): array 70 | { 71 | return $this->in; 72 | } 73 | 74 | /** @return array */ 75 | public function getOut(): array 76 | { 77 | return $this->out; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/CFG/Analysis/DataFlow/DefUses.php: -------------------------------------------------------------------------------- 1 | */ 14 | private array $blockDefs; 15 | 16 | /** @var array */ 17 | private array $blockUses; 18 | 19 | public function __construct( 20 | SymTable $symTable, 21 | CFG $cfg, 22 | ) { 23 | $this->blockDefs = []; 24 | $this->blockUses = []; 25 | 26 | $nameToId = $symTable->getNameToIdMap(); 27 | $isDef = new \SplObjectStorage(); 28 | 29 | foreach ($cfg->getBBlocks() as $block) { 30 | foreach ($block->getStmtsAndTerminator() as $stmt) { 31 | foreach (NodeUtils::definedVariables($stmt) as $var) { 32 | $isDef->attach($var); 33 | } 34 | } 35 | } 36 | 37 | foreach ($cfg->getBBlocks() as $block) { 38 | $defs = BitSet::empty(); 39 | $uses = BitSet::empty(); 40 | foreach ($block->getStmtsAndTerminator() as $stmt) { 41 | if ($stmt instanceof Variable) { 42 | if (!is_string($stmt->name)) { 43 | continue; 44 | } 45 | $varId = $nameToId[$stmt->name]; 46 | if ($isDef->contains($stmt)) { 47 | $defs->set($varId); 48 | continue; 49 | } 50 | if (!$defs->isset($varId)) { 51 | $uses->set($varId); 52 | } 53 | } 54 | } 55 | $this->blockDefs[$block->getId()] = $defs; 56 | $this->blockUses[$block->getId()] = $uses; 57 | } 58 | 59 | /* 60 | foreach ($cfg->getBBlocks() as $blockId => $_) { 61 | printf("Block %s:\n", $blockId); 62 | foreach ($this->blockDefs[$blockId]->toArray() as $varId) { 63 | printf("def: %s\n", $symTable->getName($varId)); 64 | } 65 | } 66 | */ 67 | } 68 | 69 | public static function fromCFG(CFG $cfg): self 70 | { 71 | return new self(SymTable::fromCFG($cfg), $cfg); 72 | } 73 | 74 | public function getBlockDefs(int $blockId): BitSet 75 | { 76 | return $this->blockDefs[$blockId]; 77 | } 78 | 79 | public function getBlockUses(int $blockId): BitSet 80 | { 81 | return $this->blockUses[$blockId]; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/CFG/Analysis/DataFlow/DefinitionGenKills.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class DefinitionGenKills 18 | { 19 | /** 20 | * All definitions 21 | * 22 | * This is a list of Variable nodes found in an LHS position 23 | * 24 | * @var Variable[] 25 | */ 26 | private array $defs; 27 | 28 | private BitSet $entryDefs; 29 | 30 | /** 31 | * Node -> defId 32 | * 33 | * @var SplObjectStorage 34 | */ 35 | private SplObjectStorage $stmtDefs; 36 | 37 | /** 38 | * varId -> BitSet 39 | * 40 | * @var array 41 | */ 42 | private array $varDefs; 43 | 44 | /** @var SplObjectStorage */ 45 | private SplObjectStorage $stmtGens; 46 | 47 | /** @var SplObjectStorage */ 48 | private SplObjectStorage $stmtKills; 49 | 50 | /** @var array */ 51 | private array $blockGens; 52 | 53 | /** @var array */ 54 | private array $blockKills; 55 | 56 | public function __construct( 57 | private SymTable $symTable, 58 | CFG $cfg, 59 | bool $addEntryDefinitions, 60 | ) { 61 | $this->defs = []; 62 | 63 | $this->makeDefinitions($cfg, $addEntryDefinitions); 64 | $this->makeGenKills($cfg); 65 | } 66 | 67 | public static function fromCFG(CFG $cfg): self 68 | { 69 | return new self(SymTable::fromCFG($cfg), $cfg, false); 70 | } 71 | 72 | public function getDefinition(int $id): Variable 73 | { 74 | return $this->defs[$id]; 75 | } 76 | 77 | public function getEntryDefinitions(): BitSet 78 | { 79 | return $this->entryDefs; 80 | } 81 | 82 | public function getStmtDef(Node $stmt): ?int 83 | { 84 | return $this->stmtDefs[$stmt] ?? null; 85 | } 86 | 87 | public function getBlockGens(BBlock $block): BitSet 88 | { 89 | return $this->blockGens[$block->getId()]; 90 | } 91 | 92 | public function getBlockKills(BBlock $block): BitSet 93 | { 94 | return $this->blockKills[$block->getId()]; 95 | } 96 | 97 | public function getStmtGens(Node $stmt): BitSet 98 | { 99 | return $this->stmtGens[$stmt]; 100 | } 101 | 102 | public function getStmtKills(Node $stmt): BitSet 103 | { 104 | return $this->stmtKills[$stmt]; 105 | } 106 | 107 | public function getVarDefs(int $varId): BitSet 108 | { 109 | return $this->varDefs[$varId]; 110 | } 111 | 112 | public function getSymTable(): SymTable 113 | { 114 | return $this->symTable; 115 | } 116 | 117 | private function makeDefinitions(CFG $cfg, bool $addEntryDefinitions): void 118 | { 119 | $varNameToId = $this->symTable->getNameToIdMap(); 120 | 121 | $defs = []; 122 | $entryDefs = BitSet::empty(); 123 | 124 | $varDefs = []; 125 | foreach ($varNameToId as $varName => $varId) { 126 | if ($addEntryDefinitions) { 127 | $defId = count($defs); 128 | $defs[] = new Variable($varName); 129 | $varDefs[$varId] = BitSet::unit($defId); 130 | $entryDefs->set($defId); 131 | } else { 132 | $varDefs[$varId] = BitSet::empty(); 133 | } 134 | } 135 | 136 | /** @var SplObjectStorage */ 137 | $stmtDefs = new SplObjectStorage(); 138 | 139 | foreach ($cfg->getBBlocks() as $block) { 140 | foreach ($block->getStmts() as $stmt) { 141 | foreach (NodeUtils::definedVariables($stmt) as $var) { 142 | $defId = count($defs); 143 | $defs[] = $var; 144 | $stmtDefs[$var] = $defId; 145 | if (is_string($var->name)) { // TODO 146 | $varId = $varNameToId[$var->name]; 147 | $varDefs[$varId]->set($defId); 148 | } 149 | } 150 | } 151 | } 152 | 153 | $this->defs = $defs; 154 | $this->entryDefs = $entryDefs; 155 | $this->varDefs = $varDefs; 156 | $this->stmtDefs = $stmtDefs; 157 | } 158 | 159 | private function makeGenKills(CFG $cfg): void 160 | { 161 | $defs = $this->defs; 162 | $varDefs = $this->varDefs; 163 | $stmtDefs = $this->stmtDefs; 164 | $varNameToId = $this->symTable->getNameToIdMap(); 165 | 166 | /** @var SplObjectStorage */ 167 | $stmtGens = new SplObjectStorage(); 168 | 169 | /** @var SplObjectStorage */ 170 | $stmtKills = new SplObjectStorage(); 171 | 172 | foreach ($cfg->getBBlocks() as $block) { 173 | $blockGens = BitSet::empty(); 174 | $blockKills = BitSet::empty(); 175 | foreach ($block->getStmts() as $stmt) { 176 | $def = $stmtDefs[$stmt] ?? null; 177 | if ($def === null) { 178 | $stmtGens[$stmt] = BitSet::empty(); 179 | $stmtKills[$stmt] = BitSet::empty(); 180 | continue; 181 | } 182 | $var = $defs[$def]; 183 | assert(is_string($var->name)); 184 | 185 | $gens = BitSet::unit($def); 186 | $kills = BitSet::diff($varDefs[$varNameToId[$var->name]], $gens); 187 | 188 | $stmtGens[$stmt] = $gens; 189 | $stmtKills[$stmt] = $kills; 190 | 191 | $blockKills = BitSet::union($blockKills, $kills); 192 | $blockGens = BitSet::union($gens, BitSet::diff($blockGens, $blockKills)); 193 | } 194 | $this->blockGens[$block->getId()] = $blockGens; 195 | $this->blockKills[$block->getId()] = $blockKills; 196 | } 197 | 198 | $this->stmtGens = $stmtGens; 199 | $this->stmtKills = $stmtKills; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/CFG/Analysis/DataFlow/ForwardAnalysis.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class ForwardAnalysis 14 | { 15 | /** @var array */ 16 | private array $in; 17 | 18 | /** @var array */ 19 | private array $out; 20 | 21 | /** 22 | * @param callable(V,V):V $meet 23 | * @param callable(BBlock,V):V $transfer 24 | * @param callable(V,V):bool $equals 25 | * @param V $boundary 26 | * @param V $init 27 | */ 28 | public function __construct( 29 | CFG $cfg, 30 | callable $meet, 31 | callable $transfer, 32 | callable $equals, 33 | mixed $boundary, 34 | mixed $init, 35 | ) 36 | { 37 | $entry = $cfg->getEntry(); 38 | $basicBlocks = $cfg->getBBlocks(); 39 | 40 | $out = array_fill(0, count($basicBlocks), $boundary); 41 | $out[$entry->getId()] = $init; 42 | 43 | $in = []; 44 | 45 | do { 46 | $changed = false; 47 | foreach ($basicBlocks as $id => $bb) { 48 | if ($bb === $entry) { 49 | continue; 50 | } 51 | 52 | $bbIn = $boundary; 53 | $bbOut = $out[$id]; 54 | 55 | foreach ($bb->getPredecessors() as $pred) { 56 | $bbIn = $meet($bbIn, $out[$pred->getId()]); 57 | } 58 | $in[$id] = $bbIn; 59 | $out[$id] = $transfer($bb, $in[$id]); 60 | $changed = $changed || !$equals($bbOut, $out[$id]); 61 | } 62 | } while ($changed); 63 | 64 | $this->in = $in; 65 | $this->out = $out; 66 | } 67 | 68 | /** @return array */ 69 | public function getIn(): array 70 | { 71 | return $this->in; 72 | } 73 | 74 | /** @return array */ 75 | public function getOut(): array 76 | { 77 | return $this->out; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/CFG/Analysis/DataFlow/LiveVariablesAnalysis.php: -------------------------------------------------------------------------------- 1 | */ 13 | private array $in; 14 | 15 | /** @var array */ 16 | private array $out; 17 | 18 | public function __construct( 19 | CFG $cfg, 20 | DefUses $defUses, 21 | ) { 22 | $meet = function (BitSet $a, BitSet $b): BitSet { 23 | return BitSet::union($a, $b); 24 | }; 25 | 26 | $transfer = function (BBlock $block, BitSet $x) use ($defUses): BitSet { 27 | $defs = $defUses->getBlockDefs($block->getId()); 28 | $uses = $defUses->getBlockUses($block->getId()); 29 | return BitSet::union($uses, BitSet::diff($x, $defs)); 30 | }; 31 | 32 | $equals = function (BitSet $a, BitSet $b): bool { 33 | return $a->equals($b); 34 | }; 35 | 36 | $init = BitSet::empty(); 37 | $boundary = BitSet::empty(); 38 | 39 | $bwdAnalysis = new BackwardAnalysis($cfg, $meet, $transfer, $equals, $boundary, $init); 40 | 41 | $this->in = $bwdAnalysis->getIn(); 42 | $this->out = $bwdAnalysis->getOut(); 43 | } 44 | 45 | public static function fromCFG(CFG $cfg): self 46 | { 47 | return new self($cfg, new DefUses(SymTable::fromCFG($cfg), $cfg)); 48 | } 49 | 50 | public function getBlockLiveInVariables(BBlock $block): BitSet 51 | { 52 | return $this->in[$block->getId()] ?? BitSet::empty(); 53 | } 54 | 55 | public function getBlockLiveOutVariables(BBlock $block): BitSet 56 | { 57 | return $this->out[$block->getId()]; 58 | } 59 | } -------------------------------------------------------------------------------- /src/CFG/Analysis/DataFlow/ReachingDefinitionsAnalysis.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class ReachingDefinitionsAnalysis 16 | { 17 | /** @var array */ 18 | private array $in; 19 | 20 | /** @var array */ 21 | private array $out; 22 | 23 | public function __construct( 24 | CFG $cfg, 25 | private DefinitionGenKills $genKills, 26 | BitSet $init = null, 27 | ) { 28 | $meet = function (BitSet $a, BitSet $b): BitSet { 29 | return BitSet::union($a, $b); 30 | }; 31 | 32 | $transfer = function (BBlock $block, BitSet $x) use ($genKills): BitSet { 33 | $gens = $genKills->getBlockGens($block); 34 | $kills = $genKills->getBlockKills($block); 35 | return BitSet::union($gens, BitSet::diff($x, $kills)); 36 | }; 37 | 38 | $equals = function (BitSet $a, BitSet $b): bool { 39 | return $a->equals($b); 40 | }; 41 | 42 | $init = $init ?? BitSet::empty(); 43 | $boundary = BitSet::empty(); 44 | 45 | $fwdAnalysis = new ForwardAnalysis($cfg, $meet, $transfer, $equals, $boundary, $init); 46 | 47 | $this->in = $fwdAnalysis->getIn(); 48 | $this->out = $fwdAnalysis->getOut(); 49 | } 50 | 51 | public static function fromCFG(CFG $cfg): self 52 | { 53 | return new self($cfg, new DefinitionGenKills(SymTable::fromCFG($cfg), $cfg, addEntryDefinitions: false)); 54 | } 55 | 56 | public function getBlockReachingInDefinitions(BBlock $block): BitSet 57 | { 58 | return $this->in[$block->getId()] ?? BitSet::empty(); 59 | } 60 | 61 | public function getBlockReachingOutDefinitions(BBlock $block): BitSet 62 | { 63 | return $this->out[$block->getId()]; 64 | } 65 | 66 | /** @return SplObjectStorage */ 67 | public function getStmtsReachingInDefinitions(BBlock $block): SplObjectStorage 68 | { 69 | $in = $this->in[$block->getId()] ?? null; 70 | 71 | if ($in === null) { 72 | /** @var SplObjectStorage */ 73 | $empty = new SplObjectStorage(); 74 | return $empty; 75 | } 76 | 77 | /** @var SplObjectStorage */ 78 | $perStmt = new SplObjectStorage(); 79 | 80 | foreach ($block->getStmts() as $stmt) { 81 | $lastStmt = $stmt; 82 | $perStmt[$stmt] = $in; 83 | $in = BitSet::union( 84 | BitSet::diff($in, $this->genKills->getStmtKills($stmt)), 85 | $this->genKills->getStmtGens($stmt), 86 | ); 87 | } 88 | 89 | return $perStmt; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/CFG/Analysis/DataFlow/UndefinedVariableAnalysis.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class UndefinedVariableAnalysis 16 | { 17 | /** @var SplObjectStorage */ 18 | private SplObjectStorage $defsByStmt; 19 | 20 | public function __construct( 21 | CFG $cfg, 22 | private ReachingDefinitionsAnalysis $reachingDefs, 23 | private DefinitionGenKills $genKills, 24 | private SymTable $symTable, 25 | private BitSet $entryDefs, 26 | ) 27 | { 28 | $this->defsByStmt = new SplObjectStorage(); 29 | 30 | foreach ($cfg->getBBlocks() as $block) { 31 | $this->defsByStmt->addAll($this->reachingDefs->getStmtsReachingInDefinitions($block)); 32 | } 33 | } 34 | 35 | public static function fromCFG(CFG $cfg): self 36 | { 37 | $symTable = SymTable::fromCFG($cfg); 38 | $genKills = new DefinitionGenKills($symTable, $cfg, addEntryDefinitions: true); 39 | 40 | $defs = $genKills->getEntryDefinitions(); 41 | 42 | $reachingDefs = new ReachingDefinitionsAnalysis($cfg, $genKills, $defs); 43 | 44 | return new self($cfg, $reachingDefs, $genKills, $symTable, $defs); 45 | } 46 | 47 | public function getVariableStatus(Variable $var): UndefinedVariableStatus 48 | { 49 | if (!is_string($var->name)) { 50 | return UndefinedVariableStatus::Unkonwn; 51 | } 52 | 53 | if (!$this->genKills->getStmtGens($var)->isEmpty()) { 54 | return UndefinedVariableStatus::Unkonwn; 55 | } 56 | 57 | $defs = $this->defsByStmt[$var]; 58 | 59 | $varId = $this->symTable->getId($var->name); 60 | 61 | $defs = BitSet::intersect($defs, $this->genKills->getVarDefs($varId)); 62 | 63 | $int = BitSet::intersect($defs, $this->entryDefs); 64 | 65 | if ($int->isEmpty()) { 66 | return UndefinedVariableStatus::Defined; 67 | } 68 | 69 | if ($int->count() < $defs->count()) { 70 | return UndefinedVariableStatus::MaybeUndefined; 71 | } 72 | 73 | return UndefinedVariableStatus::Undefined; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/CFG/Analysis/DataFlow/UndefinedVariableStatus.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | final class DeadCodeAnalysisSSA 22 | { 23 | /** @var SplObjectStorage */ 24 | private SplObjectStorage $deadStmts; 25 | 26 | public function __construct( 27 | private CFG $cfg, 28 | SSASymTable $symTable, 29 | ) 30 | { 31 | $sideEffectVisitor = new SideEffectVisitor(); 32 | 33 | /** @var SplObjectStorage */ 34 | $deadStmts = new SplObjectStorage(); 35 | 36 | /** @var SplObjectStorage */ 37 | $work = new SplObjectStorage(); 38 | 39 | foreach ($cfg->getBBlocks() as $block) { 40 | foreach ($block->getStmts() as $stmt) { 41 | if ($sideEffectVisitor->hasSideEffect($stmt) || in_array($stmt, $block->getTerminatorConds(), true)) { 42 | $this->processLiveStmt($stmt, $work, $symTable); 43 | 44 | continue; 45 | } 46 | 47 | $deadStmts->attach($stmt); 48 | } 49 | $terminator = $block->getTerminator(); 50 | if ($terminator instanceof Return_) { 51 | $this->processLiveStmt($terminator, $work, $symTable); 52 | } else if ($terminator instanceof Foreach_) { 53 | $deadStmts->detach($terminator->expr); 54 | $this->processLiveStmt($terminator->expr, $work, $symTable); 55 | } 56 | } 57 | 58 | while (true) { 59 | $work->rewind(); 60 | if (!$work->valid()) { 61 | break; 62 | } 63 | 64 | $stmt = $work->current(); 65 | $work->detach($stmt); 66 | 67 | if (!$deadStmts->contains($stmt)) { 68 | continue; 69 | } 70 | 71 | $deadStmts->detach($stmt); 72 | $this->processLiveStmt($stmt, $work, $symTable); 73 | } 74 | 75 | $this->deadStmts = $deadStmts; 76 | } 77 | 78 | /** @return SplObjectStorage */ 79 | public function getDeadStmts(): SplObjectStorage 80 | { 81 | return $this->deadStmts; 82 | } 83 | 84 | /** 85 | * @return SplObjectStorage 86 | */ 87 | public function getTopmostDeadStmts(): SplObjectStorage 88 | { 89 | $skip = new SplObjectStorage(); 90 | $list = []; 91 | 92 | foreach ($this->cfg->getBBlocks() as $block) { 93 | foreach (array_reverse($block->getStmts()) as $stmt) { 94 | if ($skip->contains($stmt)) { 95 | foreach (NodeUtils::childNodes($stmt) as $childNode) { 96 | $skip->attach($childNode); 97 | } 98 | continue; 99 | } 100 | if ($this->deadStmts->contains($stmt)) { 101 | if (!$stmt instanceof Phi && !$stmt instanceof Comment && !$stmt instanceof Nop) { 102 | $list[] = $stmt; 103 | } 104 | foreach (NodeUtils::childNodes($stmt) as $childNode) { 105 | $skip->attach($childNode); 106 | } 107 | } 108 | } 109 | } 110 | 111 | /** @var SplObjectStorage $store */ 112 | $store = new SplObjectStorage(); 113 | foreach (array_reverse($list) as $elem) { 114 | $store->attach($elem); 115 | } 116 | 117 | return $store; 118 | } 119 | 120 | /** @param SplObjectStorage $work */ 121 | private function processLiveStmt(Node $stmt, SplObjectStorage $work, SSASymTable $symTable): void 122 | { 123 | foreach (NodeUtils::childNodes($stmt) as $childNode) { 124 | $work->attach($childNode); 125 | } 126 | if ($stmt instanceof Variable) { 127 | $def = $symTable->getDef($stmt); 128 | if ($def !== null) { 129 | $work->attach($def); 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/CFG/Analysis/Dominance.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class Dominance 15 | { 16 | private CFG $cfg; 17 | 18 | public function __construct(CFG $cfg) 19 | { 20 | $this->cfg = $cfg; 21 | } 22 | 23 | /** 24 | * Computes the dominance frontier 25 | * 26 | * @param array $idoms block id -> immediate dominator 27 | * @return array block id -> block id set 28 | */ 29 | public function dominanceFrontier(array $idoms) 30 | { 31 | $df = []; 32 | 33 | foreach ($this->cfg->getBBlocks() as $block) { 34 | $predecessors = $block->getPredecessors(); 35 | if (count($predecessors) >= 2) { 36 | foreach ($predecessors as $p) { 37 | // FIXME: Find an other way to ignore unreachable blocks 38 | if (count($p->getPredecessors()) === 0 || !isset($idoms[$p->getId()])) { 39 | continue; 40 | } 41 | $runner = $p; 42 | while ($runner !== $idoms[$block->getId()]) { 43 | if (!isset($df[$runner->getId()])) { 44 | $df[$runner->getId()] = BitSet::unit($block->getId()); 45 | } else { 46 | $df[$runner->getId()]->set($block->getId()); 47 | } 48 | $runner = $idoms[$runner->getId()]; 49 | } 50 | } 51 | } 52 | } 53 | 54 | return $df; 55 | } 56 | 57 | /** 58 | * @param array $idoms 59 | * @return array 60 | */ 61 | public function dominatorTree(array $idoms): array 62 | { 63 | $dt = []; 64 | 65 | foreach ($this->cfg->getBBlocks() as $id => $node) { 66 | $idom = $idoms[$id] ?? null; 67 | if ($idom === null) { 68 | continue; 69 | } 70 | $dt[$idom->getId()][] = $node; 71 | } 72 | 73 | /* 74 | $graph = new Graph(); 75 | foreach ($dt as $id => $children) { 76 | $parent = $graph->createVertex($id, true); 77 | foreach ($children as $child) { 78 | $child = $graph->createVertex($child->getId(), true); 79 | $parent->createEdgeTo($child); 80 | } 81 | } 82 | $graphviz = new GraphViz(); 83 | $graphviz->display($graph); 84 | */ 85 | 86 | return $dt; 87 | } 88 | 89 | /** 90 | * Computes immediate dominators 91 | * 92 | * @return array block id -> immediate dominator 93 | */ 94 | public function immediateDominators(): array 95 | { 96 | // FIXME: check that the orders are right 97 | $postOrder = $this->cfg->getPostOrder(); 98 | $reversePostOrder = $this->cfg->getReversePostOrder(); 99 | asort($reversePostOrder); 100 | 101 | $blocks = $this->cfg->getBBlocks(); 102 | $entry = $this->cfg->getEntry(); 103 | 104 | $doms = [ 105 | $entry->getId() => $entry, 106 | ]; 107 | 108 | do { 109 | $changed = false; 110 | foreach ($reversePostOrder as $id => $_) { 111 | $b = $blocks[$id]; 112 | if ($b === $entry) { 113 | continue; 114 | } 115 | $newIdom = null; 116 | foreach ($b->getPredecessors() as $p) { 117 | if ($newIdom === null) { 118 | if (isset($doms[$p->getId()])) { 119 | $newIdom = $p; 120 | } 121 | continue; 122 | } 123 | if (isset($doms[$p->getId()])) { 124 | while ($newIdom !== $p) { 125 | while ($postOrder[$p->getId()] < $postOrder[$newIdom->getId()]) { 126 | $p = $doms[$p->getId()]; 127 | } 128 | while ($postOrder[$newIdom->getId()] < $postOrder[$p->getId()]) { 129 | $newIdom = $doms[$newIdom->getId()]; 130 | } 131 | } 132 | } 133 | } 134 | assert($newIdom !== null); 135 | if (!isset($doms[$b->getId()]) || $doms[$b->getId()] !== $newIdom) { 136 | $doms[$b->getId()] = $newIdom; 137 | $changed = true; 138 | } 139 | } 140 | } while ($changed); 141 | 142 | unset($doms[$this->cfg->getEntry()->getId()]); 143 | 144 | /* 145 | $graph = new Graph(); 146 | foreach ($doms as $id => $dom) { 147 | $parent = $graph->createVertex($id, true); 148 | $child = $graph->createVertex($dom->getId(), true); 149 | $parent->createEdgeTo($child); 150 | } 151 | $graphviz = new GraphViz(); 152 | $graphviz->display($graph); 153 | */ 154 | 155 | return $doms; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/CFG/BBlock.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | final class BBlock 26 | { 27 | /** @var Node[] */ 28 | private array $stmts; 29 | 30 | private ?string $label; 31 | 32 | private ?Node $terminator; 33 | 34 | public function __construct( 35 | private int $id, 36 | private CFG $cfg, 37 | ) 38 | { 39 | $this->stmts = []; 40 | $this->label = null; 41 | $this->terminator = null; 42 | } 43 | 44 | /** 45 | * @return array 46 | */ 47 | public function getStmts(): array 48 | { 49 | return $this->stmts; 50 | } 51 | 52 | /** 53 | * @return array 54 | */ 55 | public function getStmtsAndTerminator(): array 56 | { 57 | if ($this->terminator === null) { 58 | return $this->stmts; 59 | } 60 | 61 | return [...$this->stmts, $this->terminator]; 62 | } 63 | 64 | public function addStmt(Node $stmt): void 65 | { 66 | if ($this->terminator !== null) { 67 | throw new \RuntimeException(sprintf( 68 | 'Can not add statement in terminated block (statement: %s from line %s, terminator: %s from line %s)', 69 | get_class($stmt), 70 | $stmt->getLine(), 71 | get_class($this->terminator), 72 | $this->terminator->getLine(), 73 | )); 74 | } 75 | 76 | $this->stmts[] = $stmt; 77 | } 78 | 79 | public function prependStmt(Node $stmt): void 80 | { 81 | array_unshift($this->stmts, $stmt); 82 | } 83 | 84 | /** @param array $stmts */ 85 | public function prependStmts(array $stmts): void 86 | { 87 | $this->stmts = [...$stmts, ...$this->stmts]; 88 | } 89 | 90 | public function getId(): int 91 | { 92 | return $this->id; 93 | } 94 | 95 | /** @return BBlock[] */ 96 | public function getSuccessors(): array 97 | { 98 | return $this->cfg->getSuccessorsById($this->id); 99 | } 100 | 101 | /** @return BBlock[] */ 102 | public function getPredecessors(): array 103 | { 104 | return $this->cfg->getPredecessorsById($this->id); 105 | } 106 | 107 | public function setTerminator(Node $terminator, BBlock ...$blocks): void 108 | { 109 | if ($this->terminator !== null) { 110 | throw new \RuntimeException(sprintf( 111 | 'Can not add terminator to terminated block (statement: %s from line %s, terminator: %s from line %s)', 112 | get_class($terminator), 113 | $terminator->getLine(), 114 | get_class($this->terminator), 115 | $this->terminator->getLine(), 116 | )); 117 | } 118 | 119 | $ids = []; 120 | foreach ($blocks as $block) { 121 | $ids[] = $block->getId(); 122 | } 123 | 124 | $this->cfg->setSuccessorIds($this->id, $ids); 125 | $this->terminator = $terminator; 126 | } 127 | 128 | public function getTerminator(): ?Node 129 | { 130 | return $this->terminator; 131 | } 132 | 133 | /** @return Node[] */ 134 | public function getTerminatorConds(): array 135 | { 136 | $terminator = $this->terminator; 137 | 138 | return match (true) { 139 | $terminator === null => [], 140 | $terminator instanceof If_ => [$terminator->cond], 141 | $terminator instanceof For_ => array_slice($terminator->cond, -1), 142 | $terminator instanceof Foreach_ => [$terminator->expr], 143 | $terminator instanceof While_ => [$terminator->cond], 144 | $terminator instanceof Switch_ => [$terminator->cond], 145 | $terminator instanceof Case_ => $terminator->cond !== null ? [$terminator->cond] : [], 146 | $terminator instanceof MatchArm => $terminator->conds !== null ? $terminator->conds : [], 147 | $terminator instanceof BooleanAnd => [$terminator->left], 148 | $terminator instanceof BooleanOr => [$terminator->left], 149 | $terminator instanceof LogicalAnd => [$terminator->left], 150 | $terminator instanceof LogicalOr => [$terminator->left], 151 | $terminator instanceof Ternary => [$terminator->cond], 152 | default => [], 153 | }; 154 | } 155 | 156 | public function setSuccessor(BBlock $successor): void 157 | { 158 | $this->cfg->setSuccessorIds($this->id, [$successor->getId()]); 159 | } 160 | 161 | public function setLabel(string $label): void 162 | { 163 | $this->label = $label; 164 | } 165 | 166 | public function getLabel(): ?string 167 | { 168 | return $this->label; 169 | } 170 | 171 | public function getName(): string 172 | { 173 | return sprintf('node_%s', $this->id); 174 | } 175 | 176 | /** @return iterable */ 177 | public function reversePostOrderIterator(): iterable 178 | { 179 | foreach (array_reverse($this->getSuccessors()) as $succ) { 180 | yield from $succ->reversePostOrderIterator(); 181 | } 182 | 183 | yield $this; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/CFG/CFG.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class CFG 11 | { 12 | private int $nextNodeId; 13 | 14 | /** @var array */ 15 | private array $bblocks; 16 | 17 | private BBlock $entry; 18 | 19 | private BBlock $exit; 20 | 21 | /** @var array */ 22 | private array $edges; 23 | 24 | /** @var array */ 25 | private array $reverseEdges; 26 | 27 | public function __construct() 28 | { 29 | $this->nextNodeId = 0; 30 | $this->bblocks = []; 31 | $this->edges = []; 32 | $this->reverseEdges = []; 33 | $this->entry = $this->createBlock(); 34 | $this->exit = $this->createBlock(); 35 | $this->entry->setSuccessor($this->exit); 36 | } 37 | 38 | public function getEntry(): BBlock 39 | { 40 | return $this->entry; 41 | } 42 | 43 | public function getExit(): BBlock 44 | { 45 | return $this->exit; 46 | } 47 | 48 | public function createBlock(): BBlock 49 | { 50 | $id = $this->nextNodeId++; 51 | $block = new BBlock($id, $this); 52 | 53 | $this->bblocks[$id] = $block; 54 | 55 | return $block; 56 | } 57 | 58 | /** @return BBlock[] */ 59 | public function getBBlocks(): array 60 | { 61 | return $this->bblocks; 62 | } 63 | 64 | /** @return BBlock[] */ 65 | public function getSuccessorsById(int $id): array 66 | { 67 | $succs = []; 68 | if (!isset($this->edges[$id])) { 69 | return $succs; 70 | } 71 | 72 | foreach ($this->edges[$id] as $succId) { 73 | $succs[] = $this->bblocks[$succId]; 74 | } 75 | 76 | return $succs; 77 | } 78 | 79 | /** @return BBlock[] */ 80 | public function getPredecessorsById(int $id): array 81 | { 82 | $preds = []; 83 | if (!isset($this->reverseEdges[$id])) { 84 | return $preds; 85 | } 86 | 87 | foreach ($this->reverseEdges[$id] as $predId) { 88 | $preds[] = $this->bblocks[$predId]; 89 | } 90 | 91 | return $preds; 92 | } 93 | 94 | /** @param int[] $succs */ 95 | public function setSuccessorIds(int $pred, array $succs): void 96 | { 97 | if (isset($this->edges[$pred])) { 98 | foreach ($this->edges[$pred] as $succ) { 99 | $this->reverseEdges[$succ] = array_diff($this->reverseEdges[$succ], [$pred]); 100 | } 101 | } 102 | 103 | $this->edges[$pred] = $succs; 104 | 105 | foreach ($succs as $succ) { 106 | $this->reverseEdges[$succ][] = $pred; 107 | } 108 | } 109 | 110 | /** @param array $order */ 111 | private function getReversePostOrderImpl(int $blockId, array &$order, int &$index): void 112 | { 113 | if (isset($order[$blockId])) { 114 | return; 115 | } 116 | 117 | $order[$blockId] = $index++; 118 | 119 | if (isset($this->edges[$blockId])) { 120 | foreach ($this->edges[$blockId] as $succ) { 121 | $this->getReversePostOrderImpl($succ, $order, $index); 122 | } 123 | } 124 | 125 | } 126 | 127 | /** @return array blockId -> order */ 128 | public function getReversePostOrder(): array 129 | { 130 | $order = []; 131 | $index = 0; 132 | $this->getReversePostOrderImpl($this->entry->getId(), $order, $index); 133 | 134 | return $order; 135 | } 136 | 137 | /** @param array $order */ 138 | private function getPostOrderImpl(int $blockId, array &$order, int &$index): void 139 | { 140 | if (isset($order[$blockId])) { 141 | return; 142 | } 143 | 144 | $order[$blockId] = -1; // visiting 145 | 146 | if (isset($this->edges[$blockId])) { 147 | foreach ($this->edges[$blockId] as $succ) { 148 | $this->getPostOrderImpl($succ, $order, $index); 149 | } 150 | } 151 | 152 | $order[$blockId] = $index++; 153 | } 154 | 155 | /** @return array blockId -> order */ 156 | public function getPostOrder(): array 157 | { 158 | $order = []; 159 | $index = 0; 160 | $this->getPostOrderImpl($this->entry->getId(), $order, $index); 161 | 162 | return $order; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/CFG/CFGBuilder.php: -------------------------------------------------------------------------------- 1 | 43 | */ 44 | final class CFGBuilder 45 | { 46 | public const FLAG_DONT_ADD_USES = 1 << 0; 47 | 48 | private CFG $cfg; 49 | 50 | /** @var BBlock[] */ 51 | private array $finallyStack; 52 | 53 | /** @var BBlock[] */ 54 | private array $breakStack; 55 | 56 | /** @var BBlock[] */ 57 | private array $continueStack; 58 | 59 | /** @var array */ 60 | private array $labelBlocks; 61 | 62 | public function __construct() 63 | { 64 | $this->cfg = new CFG(); 65 | $this->finallyStack = []; 66 | $this->breakStack = []; 67 | $this->continueStack = []; 68 | $this->labelBlocks = []; 69 | } 70 | 71 | /** @param Stmt[] $stmts */ 72 | public function build(array $stmts): CFG 73 | { 74 | $entry = $this->cfg->getEntry(); 75 | $successor = $this->cfg->getExit(); 76 | $block = $this->createBlock($successor); 77 | $entry->setSuccessor($block); 78 | 79 | $this->visitList($block, $successor, $stmts); 80 | 81 | return $this->cfg; 82 | } 83 | 84 | public function buildFunction(FunctionLike $node, int $flags = 0): CFG 85 | { 86 | $entry = $this->cfg->getEntry(); 87 | $successor = $this->cfg->getExit(); 88 | $block = $this->createBlock($successor); 89 | $entry->setSuccessor($block); 90 | 91 | $block = $this->visitList($block, $successor, $node->getParams()); 92 | if ($node instanceof Closure && ($flags & self::FLAG_DONT_ADD_USES) === 0) { 93 | $block = $this->visitList($block, $successor, $node->uses); 94 | } 95 | 96 | $stmts = $node->getStmts(); 97 | if ($stmts !== null) { 98 | $this->visitList($block, $successor, $stmts); 99 | } 100 | 101 | return $this->cfg; 102 | } 103 | 104 | private function createBlock(BBlock $successor): BBlock 105 | { 106 | $block = $this->cfg->createBlock(); 107 | $block->setSuccessor($successor); 108 | 109 | return $block; 110 | } 111 | 112 | private function createBlockWithoutSuccessor(): BBlock 113 | { 114 | return $this->cfg->createBlock(); 115 | } 116 | 117 | /** @param Node[] $nodes */ 118 | private function visitList(BBlock $block, BBlock $successor, array $nodes): BBlock 119 | { 120 | foreach ($nodes as $node) { 121 | $block = $this->visit($block, $successor, $node); 122 | } 123 | 124 | return $block; 125 | } 126 | 127 | private function visit(BBlock $block, BBlock $successor, Node $node): BBlock 128 | { 129 | if ($node instanceof If_) { 130 | return $this->visitIf($block, $successor, $node); 131 | } 132 | if ($node instanceof For_) { 133 | return $this->visitFor($block, $successor, $node); 134 | } 135 | if ($node instanceof Foreach_) { 136 | return $this->visitForeach($block, $successor, $node); 137 | } 138 | if ($node instanceof While_) { 139 | return $this->visitWhile($block, $successor, $node); 140 | } 141 | if ($node instanceof Switch_) { 142 | return $this->visitSwitch($block, $successor, $node); 143 | } 144 | if ($node instanceof Match_) { 145 | return $this->visitMatch($block, $successor, $node); 146 | } 147 | if ($node instanceof Do_) { 148 | return $this->visitDo($block, $successor, $node); 149 | } 150 | if ($node instanceof BooleanAnd || $node instanceof BooleanOr || $node instanceof LogicalAnd || $node instanceof LogicalOr) { 151 | return $this->visitBooleanOp($block, $successor, $node); 152 | } 153 | if ($node instanceof Expression) { 154 | return $this->visitExpression($block, $successor, $node); 155 | } 156 | if ($node instanceof Ternary) { 157 | return $this->visitTernary($block, $successor, $node); 158 | } 159 | if ($node instanceof TryCatch) { 160 | return $this->visitTryCatch($block, $successor, $node); 161 | } 162 | if ($node instanceof Catch_) { 163 | return $this->visitCatch($block, $successor, $node); 164 | } 165 | if ($node instanceof Return_) { 166 | return $this->visitReturn($block, $successor, $node); 167 | } 168 | if ($node instanceof Continue_) { 169 | return $this->visitContinue($block, $successor, $node); 170 | } 171 | if ($node instanceof Break_) { 172 | return $this->visitBreak($block, $successor, $node); 173 | } 174 | if ($node instanceof Goto_) { 175 | return $this->visitGoto($block, $successor, $node); 176 | } 177 | if ($node instanceof Label) { 178 | return $this->visitLabel($block, $successor, $node); 179 | } 180 | if ($node instanceof FuncCall) { 181 | return $this->visitFuncCall($block, $successor, $node); 182 | } 183 | if ($node instanceof MethodCall) { 184 | return $this->visitMethodCall($block, $successor, $node); 185 | } 186 | if ($node instanceof New_) { 187 | return $this->visitNew($block, $successor, $node); 188 | } 189 | if ($node instanceof Closure) { 190 | return $this->visitClosure($block, $successor, $node); 191 | } 192 | if ($node instanceof ArrowFunction) { 193 | return $this->visitArrowFunction($block, $successor, $node); 194 | } 195 | if ($node instanceof Function_) { 196 | return $this->visitFunction($block, $successor, $node); 197 | } 198 | if ($node instanceof Class_) { 199 | return $this->visitClass($block, $successor, $node); 200 | } 201 | 202 | if ($node instanceof Expr || $node instanceof Stmt || $node instanceof Param) { 203 | foreach ($node->getSubNodeNames() as $name) { 204 | $block = $this->visitSubNode($block, $successor, $node->$name); 205 | } 206 | $block->addStmt($node); 207 | } 208 | 209 | return $block; 210 | } 211 | 212 | private function visitSubNode(BBlock $block, BBlock $successor, mixed $node): BBlock 213 | { 214 | if ($node instanceof Expr || $node instanceof Stmt) { 215 | return $this->visit($block, $successor, $node); 216 | } 217 | 218 | if (is_array($node)) { 219 | foreach ($node as $subNode) { 220 | $block = $this->visitSubNode($block, $successor, $subNode); 221 | } 222 | 223 | return $block; 224 | } 225 | 226 | return $block; 227 | } 228 | 229 | private function visitIf(BBlock $block, BBlock $successor, If_ $node): BBlock 230 | { 231 | $nextBlock = $this->createBlock($successor); 232 | $trueBlock = $this->createBlock($nextBlock); 233 | $falseBlock = $this->createBlock($nextBlock); 234 | 235 | $this->visitCondition($block, $trueBlock, $falseBlock, $node->cond, $node); 236 | $this->visitList($trueBlock, $nextBlock, $node->stmts); 237 | 238 | foreach ($node->elseifs as $elseif) { 239 | $elseTrueBlock = $this->createBlock($nextBlock); 240 | $elseFalseBlock = $this->createBlock($nextBlock); 241 | $this->visitCondition($falseBlock, $elseTrueBlock, $elseFalseBlock, $elseif->cond, $elseif); 242 | $this->visitList($elseTrueBlock, $nextBlock, $elseif->stmts); 243 | $falseBlock = $elseFalseBlock; 244 | } 245 | 246 | if ($node->else !== null) { 247 | $this->visitList($falseBlock, $nextBlock, $node->else->stmts); 248 | } 249 | 250 | return $nextBlock; 251 | } 252 | 253 | private function visitFor(BBlock $block, BBlock $successor, For_ $node): BBlock 254 | { 255 | $nextBlock = $this->createBlock($successor); 256 | $tailBlock = $this->createBlockWithoutSuccessor(); 257 | 258 | $this->breakStack[] = $nextBlock; 259 | $this->continueStack[] = $tailBlock; 260 | 261 | $block = $this->visitList($block, $successor, $node->init); 262 | 263 | $this->visitList($tailBlock, $successor, $node->loop); 264 | 265 | $bodyBlock = $this->createBlock($tailBlock); 266 | $this->visitList($bodyBlock, $tailBlock, $node->stmts); 267 | 268 | $startBlock = $this->createBlock($bodyBlock); 269 | $block->setSuccessor($startBlock); 270 | $tailBlock->setSuccessor($startBlock); 271 | 272 | $block = $this->visitList($startBlock, $successor, array_slice($node->cond, 0, -1)); 273 | $lastCond = end($node->cond); 274 | if ($lastCond !== false) { 275 | $this->visitCondition($block, $bodyBlock, $nextBlock, $lastCond, $node); 276 | } 277 | 278 | array_pop($this->breakStack); 279 | array_pop($this->continueStack); 280 | 281 | return $nextBlock; 282 | } 283 | 284 | private function visitForeach(BBlock $block, BBlock $successor, Foreach_ $node): BBlock 285 | { 286 | $nextBlock = $this->createBlock($successor); 287 | $headBlock = $this->createBlockWithoutSuccessor(); 288 | $bodyBlock = $this->createBlock($headBlock); 289 | 290 | $this->breakStack[] = $nextBlock; 291 | $this->continueStack[] = $bodyBlock; 292 | 293 | $block = $this->visit($block, $headBlock, $node->expr); 294 | $block->setSuccessor($headBlock); 295 | 296 | if ($node->keyVar !== null) { 297 | $headBlock = $this->visit($headBlock, $bodyBlock, $node->keyVar); 298 | } 299 | $headBlock = $this->visit($headBlock, $bodyBlock, $node->valueVar); 300 | $headBlock->setTerminator($node, $bodyBlock, $nextBlock); 301 | 302 | $this->visitList($bodyBlock, $headBlock, $node->stmts); 303 | 304 | array_pop($this->breakStack); 305 | array_pop($this->continueStack); 306 | 307 | return $nextBlock; 308 | } 309 | 310 | private function visitWhile(BBlock $block, BBlock $successor, While_ $node): BBlock 311 | { 312 | $nextBlock = $this->createBlock($successor); 313 | $headBlock = $this->createBlockWithoutSuccessor(); 314 | $bodyBlock = $this->createBlock($headBlock); 315 | $block->setSuccessor($headBlock); 316 | 317 | $this->breakStack[] = $nextBlock; 318 | $this->continueStack[] = $headBlock; 319 | 320 | $this->visitCondition($headBlock, $bodyBlock, $nextBlock, $node->cond, $node); 321 | $this->visitList($bodyBlock, $nextBlock, $node->stmts); 322 | 323 | array_pop($this->breakStack); 324 | array_pop($this->continueStack); 325 | 326 | return $nextBlock; 327 | } 328 | 329 | private function visitDo(BBlock $block, BBlock $successor, Do_ $node): BBlock 330 | { 331 | $nextBlock = $this->createBlock($successor); 332 | $condBlock = $this->createBlockWithoutSuccessor(); 333 | $bodyBlock = $this->createBlock($condBlock); 334 | $block->setSuccessor($bodyBlock); 335 | 336 | $this->breakStack[] = $nextBlock; 337 | $this->continueStack[] = $condBlock; 338 | 339 | $this->visitList($bodyBlock, $condBlock, $node->stmts); 340 | $this->visitCondition($condBlock, $bodyBlock, $nextBlock, $node->cond, $node); 341 | 342 | array_pop($this->breakStack); 343 | array_pop($this->continueStack); 344 | 345 | return $nextBlock; 346 | } 347 | 348 | private function visitSwitch(BBlock $block, BBlock $successor, Switch_ $node): BBlock 349 | { 350 | $nextBlock = $this->createBlock($successor); 351 | 352 | $block = $this->visit($block, $successor, $node->cond); 353 | 354 | $this->breakStack[] = $nextBlock; 355 | $this->continueStack[] = $nextBlock; 356 | 357 | $fallthrough = $nextBlock; 358 | $nextCase = $nextBlock; 359 | foreach (array_reverse($node->cases) as $case) { 360 | $bodyBlock = $this->createBlock($fallthrough); 361 | $this->visitList($bodyBlock, $fallthrough, $case->stmts); 362 | $fallthrough = $bodyBlock; 363 | 364 | if ($case->cond !== null) { 365 | $condBlock = $this->createBlockWithoutSuccessor(); 366 | $this->visitCondition($condBlock, $bodyBlock, $nextCase, $case->cond, $case); 367 | $nextCase = $condBlock; 368 | } 369 | } 370 | 371 | $block->setTerminator($node, $nextCase); 372 | 373 | array_pop($this->breakStack); 374 | array_pop($this->continueStack); 375 | 376 | return $nextBlock; 377 | } 378 | 379 | private function visitMatch(BBlock $block, BBlock $successor, Match_ $node): BBlock 380 | { 381 | $nextBlock = $this->createBlock($successor); 382 | 383 | $block = $this->visit($block, $successor, $node->cond); 384 | 385 | $nextArm = $nextBlock; 386 | foreach (array_reverse($node->arms) as $arm) { 387 | $bodyBlock = $this->createBlock($nextBlock); 388 | $this->visit($bodyBlock, $nextBlock, $arm->body); 389 | 390 | if ($arm->conds !== null) { 391 | foreach (array_reverse($arm->conds) as $cond) { 392 | $condBlock = $this->createBlockWithoutSuccessor(); 393 | $this->visitCondition($condBlock, $bodyBlock, $nextArm, $cond, $arm); 394 | $nextArm = $condBlock; 395 | } 396 | } else { 397 | $nextArm = $bodyBlock; 398 | } 399 | } 400 | 401 | $block->setTerminator($node, $nextArm); 402 | 403 | return $nextBlock; 404 | } 405 | 406 | // TODO: handle finally blocks 407 | public function visitContinue(BBlock $block, BBlock $successor, Continue_ $node): BBlock 408 | { 409 | $nextBlock = $this->createBlock($successor); 410 | 411 | if ($node->num !== null) { 412 | $num = $node->num; 413 | $block = $this->visit($block, $successor, $num); 414 | if (!$num instanceof LNumber) { 415 | $block->setTerminator($node, ...$this->continueStack); 416 | return $nextBlock; 417 | } 418 | 419 | $continueBlock = $this->continueStack[count($this->continueStack)-$num->value] ?? null; 420 | if ($continueBlock !== null) { 421 | $block->setTerminator($node, $continueBlock); 422 | return $nextBlock; 423 | } 424 | 425 | $block->setTerminator($node, ...$this->continueStack); 426 | return $nextBlock; 427 | } 428 | 429 | $continueBlock = end($this->continueStack); 430 | if ($continueBlock !== false) { 431 | $block->setTerminator($node, $continueBlock); 432 | } 433 | 434 | return $nextBlock; 435 | } 436 | 437 | // TODO: handle finally blocks 438 | public function visitBreak(BBlock $block, BBlock $successor, Break_ $node): BBlock 439 | { 440 | $nextBlock = $this->createBlock($successor); 441 | 442 | if ($node->num !== null) { 443 | $num = $node->num; 444 | $block = $this->visit($block, $successor, $num); 445 | if (!$num instanceof LNumber) { 446 | $block->setTerminator($node, ...$this->breakStack); 447 | return $nextBlock; 448 | } 449 | 450 | $breakBlock = $this->breakStack[count($this->breakStack)-$num->value] ?? null; 451 | if ($breakBlock !== null) { 452 | $block->setTerminator($node, $breakBlock); 453 | return $nextBlock; 454 | } 455 | 456 | $block->setTerminator($node, ...$this->breakStack); 457 | return $nextBlock; 458 | } 459 | 460 | $breakBlock = end($this->breakStack); 461 | if ($breakBlock !== false) { 462 | $block->setTerminator($node, $breakBlock); 463 | } 464 | 465 | return $nextBlock; 466 | } 467 | 468 | // TODO: handle finally blocks 469 | public function visitGoto(BBlock $block, BBlock $successor, Goto_ $node): BBlock 470 | { 471 | $nextBlock = $this->createBlock($successor); 472 | 473 | $labelBlock = $this->labelBlocks[$node->name->name] ?? null; 474 | if ($labelBlock === null) { 475 | $labelBlock = $this->createBlockWithoutSuccessor(); 476 | $this->labelBlocks[$node->name->name] = $labelBlock; 477 | } 478 | 479 | $block->setTerminator($node, $labelBlock); 480 | 481 | return $nextBlock; 482 | } 483 | 484 | public function visitLabel(BBlock $block, BBlock $successor, Label $node): BBlock 485 | { 486 | $labelBlock = $this->labelBlocks[$node->name->name] ?? null; 487 | if ($labelBlock === null) { 488 | $labelBlock = $this->createBlockWithoutSuccessor(); 489 | $this->labelBlocks[$node->name->name] = $labelBlock; 490 | } 491 | 492 | $block->setSuccessor($labelBlock); 493 | 494 | $labelBlock->addStmt($node); 495 | $labelBlock->setSuccessor($successor); 496 | 497 | return $labelBlock; 498 | } 499 | 500 | public function visitFuncCall(BBlock $block, BBlock $successor, FuncCall $node): BBlock 501 | { 502 | if ($node->name instanceof Expr) { 503 | $block = $this->visit($block, $successor, $node->name); 504 | } 505 | 506 | foreach ($node->args as $arg) { 507 | if ($arg instanceof Arg) { 508 | $block = $this->visit($block, $successor, $arg->value); 509 | } 510 | } 511 | 512 | $block->addStmt($node); 513 | 514 | return $block; 515 | } 516 | 517 | public function visitMethodCall(BBlock $block, BBlock $successor, MethodCall $node): BBlock 518 | { 519 | $block = $this->visit($block, $successor, $node->var); 520 | if ($node->name instanceof Expr) { 521 | $block = $this->visit($block, $successor, $node->name); 522 | } 523 | 524 | foreach ($node->args as $arg) { 525 | if ($arg instanceof Arg) { 526 | $block = $this->visit($block, $successor, $arg->value); 527 | } 528 | } 529 | 530 | $block->addStmt($node); 531 | 532 | return $block; 533 | } 534 | 535 | public function visitNew(BBlock $block, BBlock $successor, New_ $node): BBlock 536 | { 537 | if ($node->class instanceof Expr) { 538 | $block = $this->visit($block, $successor, $node->class); 539 | } 540 | 541 | foreach ($node->args as $arg) { 542 | if ($arg instanceof Arg) { 543 | $block = $this->visit($block, $successor, $arg->value); 544 | } 545 | } 546 | 547 | $block->addStmt($node); 548 | 549 | return $block; 550 | } 551 | 552 | public function visitClosure(BBlock $block, BBlock $successor, Closure $node): BBlock 553 | { 554 | foreach ($node->uses as $use) { 555 | $block = $this->visit($block, $successor, $use->var); 556 | } 557 | 558 | $block->addStmt($node); 559 | 560 | return $block; 561 | } 562 | 563 | public function visitArrowFunction(BBlock $block, BBlock $successor, ArrowFunction $node): BBlock 564 | { 565 | $builder = new CFGBuilder(); 566 | $cfg = $builder->buildFunction($node); 567 | $paramNames = []; 568 | foreach ($node->params as $param) { 569 | if ($param->var instanceof Variable) { 570 | assert(is_string($param->var->name)); 571 | $paramNames[] = $param->var->name; 572 | } 573 | } 574 | 575 | foreach ($cfg->getBBlocks() as $fnBlock) { 576 | foreach ($fnBlock->getStmts() as $stmt) { 577 | if ($stmt instanceof Variable && !in_array($stmt->name, $paramNames, true)) { 578 | $block->addStmt($stmt); 579 | } 580 | } 581 | } 582 | 583 | $block->addStmt($node); 584 | 585 | return $block; 586 | } 587 | 588 | public function visitFunction(BBlock $block, BBlock $successor, Function_ $node): BBlock 589 | { 590 | $block->addStmt($node); 591 | 592 | return $block; 593 | } 594 | 595 | public function visitClass(BBlock $block, BBlock $successor, Class_ $node): BBlock 596 | { 597 | $block->addStmt($node); 598 | 599 | return $block; 600 | } 601 | 602 | public function visitBooleanOp(BBlock $block, BBlock $successor, Expr|Stmt $node): BBlock 603 | { 604 | $confluenceBlock = $this->createBlock($successor); 605 | $this->visitShortcuttingOp($block, $confluenceBlock, $confluenceBlock, $node, null); 606 | 607 | return $confluenceBlock; 608 | } 609 | 610 | public function visitExpression(BBlock $block, BBlock $successor, Expression $node): BBlock 611 | { 612 | return $this->visit($block, $successor, $node->expr); 613 | } 614 | 615 | private function visitTernary(BBlock $block, BBlock $successor, Ternary $node): BBlock 616 | { 617 | $nextBlock = $this->createBlock($successor); 618 | $trueBlock = $node->if !== null ? $this->createBlock($nextBlock) : $nextBlock; 619 | $falseBlock = $this->createBlock($nextBlock); 620 | 621 | $this->visitCondition($block, $trueBlock, $falseBlock, $node->cond, $node); 622 | 623 | if ($node->if !== null) { 624 | $this->visit($trueBlock, $nextBlock, $node->if); 625 | } 626 | $this->visit($falseBlock, $nextBlock, $node->else); 627 | 628 | $nextBlock->addStmt($node); 629 | 630 | return $nextBlock; 631 | } 632 | 633 | // TODO: Any statement could jump to all catch blocks. Should we model that 634 | // in the CFG, or should we have special handling of try/catch in analyses ? 635 | private function visitTryCatch(BBlock $block, BBlock $successor, TryCatch $node): BBlock 636 | { 637 | $nextBlock = $this->createBlock($successor); 638 | $finallyBlock = null; 639 | 640 | if ($node->finally !== null) { 641 | $finallyBlock = $this->createBlock($nextBlock); 642 | $this->visitList($finallyBlock, $nextBlock, $node->finally->stmts); 643 | $nextBlock = $finallyBlock; 644 | $this->finallyStack[] = $finallyBlock; 645 | $this->continueStack[] = $finallyBlock; 646 | $this->breakStack[] = $finallyBlock; 647 | } 648 | 649 | $tryBlock = $this->createBlock($nextBlock); 650 | $this->visitList($tryBlock, $nextBlock, $node->stmts); 651 | 652 | $catchBlocks = []; 653 | foreach ($node->catches as $catch) { 654 | $catchBlock = $this->createBlock($successor); 655 | $this->visit($catchBlock, $nextBlock, $catch); 656 | } 657 | 658 | if ($finallyBlock !== null) { 659 | array_pop($this->finallyStack); 660 | array_pop($this->continueStack); 661 | array_pop($this->breakStack); 662 | } 663 | 664 | $block->setTerminator($node, $tryBlock, ...$catchBlocks); 665 | 666 | return $nextBlock; 667 | } 668 | 669 | private function visitCatch(BBlock $block, BBlock $successor, Catch_ $node): BBlock 670 | { 671 | if ($node->var !== null) { 672 | $block = $this->visit($block, $successor, $node->var); 673 | } 674 | 675 | $nextBlock = $this->createBlock($successor); 676 | $catchBlock = $this->createBlock($successor); 677 | 678 | $block->setTerminator($node, $catchBlock, $nextBlock); 679 | 680 | return $this->visitList($catchBlock, $nextBlock, $node->stmts); 681 | } 682 | 683 | private function visitReturn(BBlock $block, BBlock $successor, Return_ $node): BBlock 684 | { 685 | if ($node->expr !== null) { 686 | $block = $this->visit($block, $successor, $node->expr); 687 | } 688 | 689 | $finallyBlock = end($this->finallyStack); 690 | $block->setTerminator($node, $finallyBlock !== false ? $finallyBlock : $this->cfg->getExit()); 691 | 692 | return $this->createBlock($successor); 693 | } 694 | 695 | private function visitCondition(BBlock $block, BBlock $trueBlock, BBlock $falseBlock, Expr|Stmt $node, Node $parent): void 696 | { 697 | $this->visitShortcuttingOp($block, $trueBlock, $falseBlock, $node, $parent); 698 | } 699 | 700 | private function visitShortcuttingOp(BBlock $block, BBlock $trueBlock, BBlock $falseBlock, Expr|Stmt $node, ?Node $parent): void 701 | { 702 | if ($node instanceof BooleanAnd || $node instanceof LogicalAnd) { 703 | $rightBlock = $this->createBlock($trueBlock); 704 | $this->visitShortcuttingOp($block, $rightBlock, $falseBlock, $node->left, $node); 705 | $this->visitShortcuttingOp($rightBlock, $trueBlock, $falseBlock, $node->right, $parent); 706 | return; 707 | } 708 | 709 | if ($node instanceof BooleanOr || $node instanceof LogicalOr) { 710 | $rightBlock = $this->createBlock($trueBlock); 711 | $this->visitShortcuttingOp($block, $trueBlock, $rightBlock, $node->left, $node); 712 | $this->visitShortcuttingOp($rightBlock, $trueBlock, $falseBlock, $node->right, $parent); 713 | return; 714 | } 715 | 716 | $block = $this->visit($block, $block, $node); 717 | if ($parent !== null) { 718 | $block->setTerminator($parent, $trueBlock, $falseBlock); 719 | } 720 | } 721 | 722 | } 723 | -------------------------------------------------------------------------------- /src/CFG/NodeContext.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | final class NodeContext 20 | { 21 | private const READ = 1<<0; 22 | private const WRITE = 1<<1; 23 | 24 | /** @param SplObjectStorage $context */ 25 | private function __construct( 26 | private SplObjectStorage $context 27 | ) { 28 | } 29 | 30 | public static function fromCFG(CFG $cfg): self 31 | { 32 | /** @var SplObjectStorage $context */ 33 | $context = new SplObjectStorage(); 34 | 35 | foreach ($cfg->getBBlocks() as $block) { 36 | self::addBlock($context, $block); 37 | } 38 | 39 | return new self($context); 40 | } 41 | 42 | public static function fromBlock(BBlock $block): self 43 | { 44 | /** @var SplObjectStorage $context */ 45 | $context = new SplObjectStorage(); 46 | 47 | self::addBlock($context, $block); 48 | 49 | return new self($context); 50 | } 51 | 52 | public function isWrite(Node $node): bool 53 | { 54 | return (($this->context[$node] ?? 0) & self::WRITE) !== 0; 55 | } 56 | 57 | /** @param SplObjectStorage $context */ 58 | private static function addBlock(SplObjectStorage $context, BBlock $block): void 59 | { 60 | $terminator = $block->getTerminator(); 61 | if ($terminator instanceof Foreach_) { 62 | if ($terminator->keyVar !== null) { 63 | $context->attach($terminator->keyVar, self::WRITE); 64 | } 65 | $context->attach($terminator->valueVar, self::WRITE); 66 | } 67 | 68 | foreach (array_reverse($block->getStmts()) as $stmt) { 69 | switch (true) { 70 | case $stmt instanceof Assign: 71 | $context->attach($stmt->var, self::WRITE); 72 | break; 73 | case $stmt instanceof Phi: 74 | $context->attach($stmt->var, self::WRITE); 75 | break; 76 | case $stmt instanceof PostInc || $stmt instanceof PostDec || $stmt instanceof PreInc || $stmt instanceof PreDec: 77 | $context->attach($stmt->var, self::WRITE | self::READ); 78 | break; 79 | case $stmt instanceof Param: 80 | $context->attach($stmt->var, self::WRITE); 81 | // TODO 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/CFG/NodeUtils.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | final class NodeUtils 36 | { 37 | /** @return iterable */ 38 | public static function childNodes(Node $node): iterable 39 | { 40 | yield from match(true) { 41 | $node instanceof Class_ => [], 42 | $node instanceof Function_ => [], 43 | $node instanceof Closure => $node->uses, 44 | default => self::genericChildNodes($node), 45 | }; 46 | } 47 | 48 | /** @return iterable */ 49 | private static function genericChildNodes(Node $node): iterable 50 | { 51 | foreach ($node->getSubNodeNames() as $childName) { 52 | $childNode = $node->$childName; 53 | if ($childNode instanceof Stmt || $childNode instanceof Expr) { 54 | yield $childNode; 55 | } 56 | if (is_array($childNode)) { 57 | foreach ($childNode as $elem) { 58 | if ($elem instanceof Stmt || $elem instanceof Expr) { 59 | yield $elem; 60 | } 61 | if ($elem instanceof Arg) { 62 | yield $elem->value; 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | /** @return iterable */ 70 | public static function deepChildNodes(Node $node): iterable 71 | { 72 | foreach (self::childNodes($node) as $childNode) { 73 | 74 | yield $childNode; 75 | yield from self::deepChildNodes($childNode); 76 | } 77 | } 78 | 79 | /** @return iterable */ 80 | public static function definedVariables(Node $stmt): iterable 81 | { 82 | return match (true) { 83 | $stmt instanceof Assign => self::lhsToVariables($stmt->var), 84 | $stmt instanceof AssignRef => self::lhsToVariables($stmt->var), 85 | $stmt instanceof Phi => self::lhsToVariables($stmt->var), 86 | $stmt instanceof PreInc && $stmt->var instanceof Variable => [$stmt->var], 87 | $stmt instanceof PreDec && $stmt->var instanceof Variable => [$stmt->var], 88 | $stmt instanceof PostInc && $stmt->var instanceof Variable => [$stmt->var], 89 | $stmt instanceof PostDec && $stmt->var instanceof Variable => [$stmt->var], 90 | $stmt instanceof Param && $stmt->var instanceof Variable => [$stmt->var], 91 | $stmt instanceof ClosureUse => [$stmt->var], 92 | $stmt instanceof Static_ => array_map( 93 | fn ($staticVar) => $staticVar->var, 94 | $stmt->vars, 95 | ), 96 | $stmt instanceof Global_ => array_filter( 97 | $stmt->vars, 98 | fn ($expr) => $expr instanceof Variable, 99 | ), 100 | $stmt instanceof Foreach_ => [ 101 | ...($stmt->keyVar !== null ? self::lhsToVariables($stmt->keyVar) : []), 102 | ...self::lhsToVariables($stmt->valueVar), 103 | ], 104 | $stmt instanceof Unset_ => array_filter( 105 | $stmt->vars, 106 | fn ($expr) => $expr instanceof Variable, 107 | ), 108 | default => [], 109 | }; 110 | } 111 | 112 | /** 113 | * Finds the variables that are defined when the $node is in RHS of an expression 114 | * 115 | * @return iterable 116 | */ 117 | public static function lhsToVariables(Node $node): iterable 118 | { 119 | return match (true) { 120 | $node instanceof Variable => [$node], 121 | $node instanceof PropertyFetch => [], 122 | $node instanceof ArrayDimFetch => [], 123 | $node instanceof Array_ => self::lhsArrayToVariables($node), 124 | $node instanceof StaticPropertyFetch => [], 125 | default => throw new \Exception(sprintf('TODO: %s', get_class($node))), 126 | }; 127 | } 128 | 129 | /** @return iterable */ 130 | private static function lhsArrayToVariables(Array_ $node): iterable 131 | { 132 | foreach ($node->items as $item) { 133 | if ($item === null) { 134 | continue; 135 | } 136 | yield from self::lhsToVariables($item->value); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/CFG/SSA/Conversion/BlockDefUse.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class BlockDefUse 16 | { 17 | private BitSet $def; 18 | 19 | private BitSet $use; 20 | 21 | public function __construct( 22 | SymTable $symTable, 23 | BBlock $block, 24 | ) { 25 | $this->def = BitSet::empty(); 26 | $this->use = BitSet::empty(); 27 | 28 | $nameToId = $symTable->getNameToIdMap(); 29 | $context = NodeContext::fromBlock($block); 30 | 31 | $terminator = $block->getTerminator(); 32 | if ($terminator instanceof Foreach_) { 33 | $var = $terminator->keyVar; 34 | if ($var instanceof Variable && is_string($var->name)) { 35 | $id = $nameToId[$var->name]; 36 | $this->def->set($id); 37 | } 38 | $var = $terminator->valueVar; 39 | if ($var instanceof Variable && is_string($var->name)) { 40 | $id = $nameToId[$var->name]; 41 | $this->def->set($id); 42 | } 43 | } 44 | 45 | foreach ($block->getStmts() as $stmt) { 46 | if ($stmt instanceof Variable) { 47 | if (!is_string($stmt->name)) { 48 | continue; 49 | } 50 | $id = $nameToId[$stmt->name]; 51 | if ($context->isWrite($stmt)) { 52 | $this->def->set($id); 53 | continue; 54 | } 55 | if (!$this->def->isset($id)) { 56 | $this->use->set($id); 57 | } 58 | } 59 | } 60 | } 61 | 62 | public function getDef(): BitSet 63 | { 64 | return $this->def; 65 | } 66 | 67 | public function getUse(): BitSet 68 | { 69 | return $this->use; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/CFG/SSA/Conversion/RenameProcess.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | final class RenameProcess 30 | { 31 | /** @var array */ 32 | private array $stacks; 33 | 34 | /** @var array */ 35 | private array $stackPos; 36 | 37 | private SSASymTable $ssaSymTable; 38 | 39 | /** @var SplObjectStorage */ 40 | private SplObjectStorage $defs; 41 | 42 | /** @param array $domTree */ 43 | public function __construct( 44 | private CFG $cfg, 45 | private SymTable $symTable, 46 | private array $domTree, 47 | ) 48 | { 49 | $this->stacks = []; 50 | $this->stackPos = []; 51 | $this->ssaSymTable = new SSASymTable(); 52 | $this->defs = new SplObjectStorage(); 53 | } 54 | 55 | public function rename(): void 56 | { 57 | foreach ($this->symTable->getIdToNameMap() as $varName) { 58 | $this->addImplicitVar($varName); 59 | } 60 | $this->visitBlock($this->cfg->getEntry()); 61 | } 62 | 63 | public function getSymbolTable(): SSASymTable 64 | { 65 | return $this->ssaSymTable; 66 | } 67 | 68 | private function visitBlock(BBlock $bb): void 69 | { 70 | $stackPos = $this->stackPos; 71 | foreach ($bb->getStmts() as $stmt) { 72 | foreach (NodeUtils::definedVariables($stmt) as $node) { 73 | $this->defs->attach($node, $stmt); 74 | } 75 | } 76 | foreach ($bb->getStmts() as $stmt) { 77 | $this->visitStmt($stmt); 78 | } 79 | foreach ($bb->getSuccessors() as $y) { 80 | $j = null; 81 | foreach ($y->getStmts() as $stmt) { 82 | if (!$stmt instanceof Phi) { 83 | if (!$stmt instanceof Variable) { 84 | break; 85 | } 86 | continue; 87 | } 88 | if ($j === null) { 89 | $j = $this->whichPred($y, $bb); 90 | } 91 | assert(is_string($stmt->var->name)); 92 | $id = $this->stackTop($stmt->var->name); 93 | $stmt->sources[$j]->setAttribute('ssa_var', $id); 94 | } 95 | } 96 | // FIXME: we nay not need this if we ignore unreachable blocks 97 | if (isset($this->domTree[$bb->getId()])) { 98 | foreach ($this->domTree[$bb->getId()] as $child) { 99 | $this->visitBlock($child); 100 | } 101 | } 102 | $this->stackPos = $stackPos; 103 | } 104 | 105 | private function whichPred(BBlock $x, BBlock $y): int 106 | { 107 | foreach ($x->getPredecessors() as $j => $p) { 108 | if ($p === $y) { 109 | return $j; 110 | } 111 | } 112 | 113 | throw new RuntimeException('Should not happen'); 114 | } 115 | 116 | private function visitStmt(Node $stmt): void 117 | { 118 | if (!$stmt instanceof Variable) { 119 | return; 120 | } 121 | 122 | assert(is_string($stmt->name)); 123 | $def = $this->defs[$stmt] ?? null; 124 | 125 | if ($def !== null) { 126 | if ($def instanceof PreInc || $def instanceof PreDec || $def instanceof PostInc || $def instanceof PostDec) { 127 | $id = $this->stackTop($stmt->name); 128 | $stmt->setAttribute('ssa_var', $id); 129 | $this->addVar($stmt->name, $stmt); 130 | 131 | return; 132 | } 133 | 134 | $id = $this->addVar($stmt->name, $stmt); 135 | $stmt->setAttribute('ssa_var', $id); 136 | 137 | return; 138 | } 139 | 140 | $id = $this->stackTop($stmt->name); 141 | $stmt->setAttribute('ssa_var', $id); 142 | } 143 | 144 | private function stackTop(string $varName): int 145 | { 146 | if (!isset($this->stackPos[$varName])) { 147 | throw new \Exception(sprintf('Undefined variable "%s"', $varName)); 148 | } 149 | return $this->stacks[$varName][$this->stackPos[$varName]] ?? 0; 150 | } 151 | 152 | private function stackPush(string $varName, int $id): void 153 | { 154 | $stackPos = ($this->stackPos[$varName] ?? -1) + 1; 155 | $this->stackPos[$varName] = $stackPos; 156 | $this->stacks[$varName][$stackPos] = $id; 157 | } 158 | 159 | private function addVar(string $varName, Node $def): int 160 | { 161 | $id = $this->ssaSymTable->addDef($def); 162 | $this->stackPush($varName, $id); 163 | 164 | return $id; 165 | } 166 | 167 | private function addImplicitVar(string $varName): void 168 | { 169 | $id = $this->ssaSymTable->addImplicitDef(); 170 | $this->stackPush($varName, $id); 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /src/CFG/SSA/Conversion/SSAConversion.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class SSAConversion 22 | { 23 | public function __construct( 24 | private CFG $cfg, 25 | private SymTable $symTable, 26 | ) { 27 | } 28 | 29 | public static function convert(CFG $cfg): SSASymTable 30 | { 31 | $symTable = SymTable::fromCFG($cfg); 32 | 33 | $conversion = new self($cfg, $symTable); 34 | 35 | $defUses = []; 36 | foreach ($cfg->getBBlocks() as $block) { 37 | $defUses[$block->getId()] = new BlockDefUse($symTable, $block); 38 | } 39 | 40 | $dom = new Dominance($cfg); 41 | 42 | $idoms = $dom->immediateDominators(); 43 | $df = $dom->dominanceFrontier($idoms); 44 | $domTree = $dom->dominatorTree($idoms); 45 | 46 | $conversion->insertPhis($defUses, $df); 47 | $symTable = $conversion->rename($domTree); 48 | 49 | return $symTable; 50 | } 51 | 52 | /** 53 | * Place phi functions 54 | * 55 | * @param array $defUses 56 | * @param array $df 57 | */ 58 | public function insertPhis(array $defUses, array $df): void 59 | { 60 | $nodes = $this->cfg->getBBlocks(); 61 | $phi = []; 62 | $variables = $this->symTable->getIdToNameMap(); 63 | $orig = []; 64 | $defsites = []; 65 | foreach ($variables as $varId => $varName) { 66 | $phi[$varId] = BitSet::empty(); 67 | $defsites[$varId] = []; 68 | } 69 | foreach ($defUses as $bbId => $defUse) { 70 | $orig[$bbId] = BitSet::empty(); 71 | foreach ($defUse->getDef()->toArray() as $varId) { 72 | $orig[$bbId]->set($varId); 73 | $defsites[$varId][] = $bbId; 74 | } 75 | } 76 | foreach ($variables as $v => $varName) { 77 | $w = $defsites[$v]; 78 | while (true) { 79 | $x = array_pop($w); 80 | if ($x === null) { 81 | break; 82 | } 83 | if (!isset($df[$x])) { 84 | continue; 85 | } 86 | foreach ($df[$x]->toArray() as $y) { 87 | if ($phi[$v]->isset($y)) { 88 | continue; 89 | } 90 | 91 | $varNode = new Variable($varName); 92 | $sourceNodes = []; 93 | for ($i = 0, $l = count($nodes[$y]->getPredecessors()); $i < $l; $i++) { 94 | $sourceNodes[] = new Variable($varName); 95 | } 96 | $nodes[$y]->prependStmts([ 97 | $varNode, 98 | ...$sourceNodes, 99 | new Phi($varNode, $sourceNodes), 100 | ]); 101 | 102 | $phi[$v]->set($y); 103 | if (!$orig[$y]->isset($v)) { 104 | $w[] = $y; 105 | } 106 | } 107 | } 108 | } 109 | } 110 | 111 | /** 112 | * SSA rename vars 113 | * 114 | * @param array $domTree 115 | */ 116 | public function rename(array $domTree): SSASymTable 117 | { 118 | $process = new RenameProcess($this->cfg, $this->symTable, $domTree); 119 | $process->rename(); 120 | 121 | return $process->getSymbolTable(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/CFG/SSA/Node/Phi.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | final class Phi extends Stmt 12 | { 13 | public Variable $var; 14 | 15 | /** @var Variable[] */ 16 | public array $sources; 17 | 18 | /** @param array $sources */ 19 | public function __construct(Variable $var, array $sources) 20 | { 21 | parent::__construct(); 22 | $this->var = $var; 23 | $this->sources = $sources; 24 | } 25 | 26 | /** @return string[] */ 27 | public function getSubNodeNames(): array 28 | { 29 | return ['var', 'sources']; 30 | } 31 | 32 | public function getType(): string 33 | { 34 | return 'Stmt_Phi'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/CFG/SSA/SSASymTable.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | final class SSASymTable 12 | { 13 | /** @var array */ 14 | private array $idToDef; 15 | 16 | public function __construct() 17 | { 18 | $this->idToDef = []; 19 | } 20 | 21 | /** 22 | * Add a variable and return its unique id 23 | * 24 | * @param Node $def The definition node (e.g. an Assign node) 25 | */ 26 | public function addDef(Node $def): int 27 | { 28 | $id = count($this->idToDef); 29 | $this->idToDef[] = $def; 30 | 31 | return $id; 32 | } 33 | 34 | public function addImplicitDef(): int 35 | { 36 | $id = count($this->idToDef); 37 | $this->idToDef[] = null; 38 | 39 | return $id; 40 | } 41 | 42 | /** 43 | * Returns the definition node of a variable (e.g. an Assign node) or null if the variable is undefined 44 | */ 45 | public function getDef(int|Variable $var): ?Node 46 | { 47 | if ($var instanceof Variable) { 48 | $var = SSAUtils::varId($var); 49 | } 50 | 51 | if (!array_key_exists($var, $this->idToDef)) { 52 | throw new \Exception(sprintf( 53 | 'Invalid variable %d', 54 | $var, 55 | )); 56 | } 57 | 58 | return $this->idToDef[$var]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/CFG/SSA/SSAUtils.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class SSAUtils 11 | { 12 | public static function varId(Variable $var): int 13 | { 14 | $id = $var->getAttribute('ssa_var'); 15 | if ($id === null) { 16 | throw new \Exception('Variable has no SSA id'); 17 | } 18 | if (!is_int($id)) { 19 | throw new \Exception('SSA id has invalid type'); 20 | } 21 | return $id; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/CFG/SymTable.php: -------------------------------------------------------------------------------- 1 | */ 11 | private array $idToName; 12 | 13 | /** @var array */ 14 | private array $nameToId; 15 | 16 | public function __construct() 17 | { 18 | $this->idToName = []; 19 | $this->nameToId = []; 20 | } 21 | 22 | /** @return int Variable id */ 23 | public function addVar(string $name): int 24 | { 25 | if (isset($this->nameToId[$name])) { 26 | return $this->nameToId[$name]; 27 | } 28 | 29 | $id = count($this->idToName); 30 | $this->idToName[] = $name; 31 | $this->nameToId[$name] = $id; 32 | 33 | return $id; 34 | } 35 | 36 | public function getId(string $name): int 37 | { 38 | if (!isset($this->nameToId[$name])) { 39 | throw new \Exception(sprintf( 40 | 'Unknown var: "%s"', 41 | $name, 42 | )); 43 | } 44 | 45 | return $this->nameToId[$name]; 46 | } 47 | 48 | public function getName(int $id): string 49 | { 50 | if (!isset($this->idToName[$id])) { 51 | throw new \Exception(sprintf( 52 | 'Unknown var id: %d', 53 | $id, 54 | )); 55 | } 56 | 57 | return $this->idToName[$id]; 58 | } 59 | 60 | /** @return array */ 61 | public function getIdToNameMap(): array 62 | { 63 | return $this->idToName; 64 | } 65 | 66 | /** @return array */ 67 | public function getNameToIdMap(): array 68 | { 69 | return $this->nameToId; 70 | } 71 | 72 | public static function fromCFG(CFG $cfg): SymTable 73 | { 74 | $symTable = new SymTable(); 75 | 76 | foreach ($cfg->getBBlocks() as $block) { 77 | $terminator = $block->getTerminator(); 78 | if ($terminator instanceof Foreach_) { 79 | $var = $terminator->keyVar; 80 | if ($var instanceof Variable && is_string($var->name)) { 81 | $symTable->addVar($var->name); 82 | } 83 | $var = $terminator->valueVar; 84 | if ($var instanceof Variable && is_string($var->name)) { 85 | $symTable->addVar($var->name); 86 | } 87 | } 88 | foreach ($block->getStmts() as $stmt) { 89 | if ($stmt instanceof Variable) { 90 | if (is_string($stmt->name)) { 91 | $symTable->addVar($stmt->name); 92 | } 93 | } 94 | } 95 | } 96 | 97 | return $symTable; 98 | } 99 | } -------------------------------------------------------------------------------- /src/Utils/BitSet.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | final class BitSet 9 | { 10 | /** @param int[] $bits */ 11 | private function __construct( 12 | private array $bits, 13 | ) { 14 | } 15 | 16 | public static function empty(): BitSet 17 | { 18 | return new BitSet([]); 19 | } 20 | 21 | public static function unit(int $bit): BitSet 22 | { 23 | assert($bit >= 0); 24 | 25 | $bits = []; 26 | 27 | for ( 28 | $size = \intval(($bit + (\PHP_INT_SIZE * 8)) / (\PHP_INT_SIZE * 8)); 29 | $size > 0; 30 | $size-- 31 | ) { 32 | $bits[] = 0; 33 | } 34 | 35 | $index = \intval($bit / (\PHP_INT_SIZE * 8)); 36 | $bit = \intval($bit % (\PHP_INT_SIZE * 8)); 37 | $bits[$index] |= 1 << $bit; 38 | 39 | return new BitSet($bits); 40 | } 41 | 42 | 43 | /** @param int[] $bitList */ 44 | public static function fromArray(array $bitList): BitSet 45 | { 46 | if (count($bitList) === 0) { 47 | return new BitSet([]); 48 | } 49 | 50 | $bits = []; 51 | 52 | for ( 53 | $size = \intval((\max($bitList) + (\PHP_INT_SIZE * 8)) / (\PHP_INT_SIZE * 8)); 54 | $size > 0; 55 | $size-- 56 | ) { 57 | $bits[] = 0; 58 | } 59 | 60 | foreach ($bitList as $bit) { 61 | $index = \intval($bit / (\PHP_INT_SIZE * 8)); 62 | $bit = \intval($bit % (\PHP_INT_SIZE * 8)); 63 | $bits[$index] |= 1 << $bit; 64 | } 65 | 66 | return new BitSet($bits); 67 | } 68 | 69 | public function set(int $bit): void 70 | { 71 | $index = \intval($bit / (\PHP_INT_SIZE * 8)); 72 | 73 | for ($size = \count($this->bits); $size <= $index; $size++) { 74 | $this->bits[] = 0; 75 | } 76 | 77 | $bit = \intval($bit % (\PHP_INT_SIZE * 8)); 78 | 79 | $this->bits[$index] |= 1 << $bit; 80 | } 81 | 82 | public function isset(int $bit): bool 83 | { 84 | $index = \intval($bit / (\PHP_INT_SIZE * 8)); 85 | $bit = \intval($bit % (\PHP_INT_SIZE * 8)); 86 | 87 | return (($this->bits[$index] ?? 0) & (1 << $bit)) !== 0; 88 | } 89 | 90 | public function unset(int $bit): void 91 | { 92 | $index = \intval($bit / (\PHP_INT_SIZE * 8)); 93 | 94 | for ($size = \count($this->bits); $size <= $index; $size++) { 95 | $this->bits[] = 0; 96 | } 97 | 98 | $bit = \intval($bit % (\PHP_INT_SIZE * 8)); 99 | 100 | $this->bits[$index] &= ~(1 << $bit); 101 | } 102 | 103 | public function equals(BitSet $other): bool 104 | { 105 | $aBits = $this->bits; 106 | $bBits = $other->bits; 107 | 108 | for ($i = 0, $l = \max(\count($aBits), \count($bBits)); $i < $l; $i++) { 109 | if (($aBits[$i] ?? 0) !== ($bBits[$i] ?? 0)) { 110 | return false; 111 | } 112 | } 113 | 114 | return true; 115 | } 116 | 117 | public function overlaps(BitSet $other): bool 118 | { 119 | $aBits = $this->bits; 120 | $bBits = $other->bits; 121 | 122 | for ($i = 0, $l = \max(\count($aBits), \count($bBits)); $i < $l; $i++) { 123 | if ((($aBits[$i] ?? 0) | ($bBits[$i] ?? 0)) !== 0) { 124 | return true; 125 | } 126 | } 127 | 128 | return false; 129 | } 130 | 131 | public function isEmpty(): bool 132 | { 133 | if (count($this->bits) === 0) { 134 | return true; 135 | } 136 | 137 | foreach ($this->bits as $elem) { 138 | if ($elem !== 0) { 139 | return false; 140 | } 141 | } 142 | 143 | return true; 144 | } 145 | 146 | public function count(): int 147 | { 148 | $count = 0; 149 | 150 | foreach ($this->bits as $elem) { 151 | for ($i = 0; $i < PHP_INT_SIZE * 8; $i++) { 152 | if (($elem & (1 << $i)) !== 0) { 153 | $count++; 154 | } 155 | } 156 | } 157 | 158 | return $count; 159 | } 160 | 161 | /** @return array */ 162 | public function toArray(): array 163 | { 164 | $array = []; 165 | 166 | foreach ($this->bits as $index => $elem) { 167 | for ($i = 0; $i < PHP_INT_SIZE * 8; $i++) { 168 | if (($elem & (1 << $i)) !== 0) { 169 | $array[] = $index * (PHP_INT_SIZE * 8) + $i; 170 | } 171 | } 172 | } 173 | 174 | return $array; 175 | } 176 | 177 | public static function union(BitSet $a, BitSet $b): BitSet 178 | { 179 | $bits = []; 180 | $aBits = $a->bits; 181 | $bBits = $b->bits; 182 | 183 | for ($i = 0, $l = \max(\count($aBits), \count($bBits)); $i < $l; $i++) { 184 | $bits[] = ($aBits[$i] ?? 0) | ($bBits[$i] ?? 0); 185 | } 186 | 187 | return new BitSet($bits); 188 | } 189 | 190 | public static function intersect(BitSet $a, BitSet $b): BitSet 191 | { 192 | $bits = []; 193 | $aBits = $a->bits; 194 | $bBits = $b->bits; 195 | 196 | for ($i = 0, $l = \min(\count($aBits), \count($bBits)); $i < $l; $i++) { 197 | $bits[] = ($aBits[$i] ?? 0) & ($bBits[$i] ?? 0); 198 | } 199 | 200 | return new BitSet($bits); 201 | } 202 | 203 | public static function diff(BitSet $a, BitSet $b): BitSet 204 | { 205 | $bits = []; 206 | $aBits = $a->bits; 207 | $bBits = $b->bits; 208 | 209 | for ($i = 0, $l = \max(\count($aBits), \count($bBits)); $i < $l; $i++) { 210 | $bits[] = ($aBits[$i] ?? 0) & ~($bBits[$i] ?? 0); 211 | } 212 | 213 | return new BitSet($bits); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/Utils/FunctionUtils.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class FunctionUtils 13 | { 14 | static function getName(FunctionLike $function): ?string 15 | { 16 | return match (true) { 17 | $function instanceof Function_ => $function->name->name, 18 | $function instanceof ClassMethod => $function->name->name, 19 | default => null, 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Utils/SideEffectVisitor.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | final class SideEffectVisitor 31 | { 32 | public function hasSideEffect(Node $stmt): bool 33 | { 34 | return match (true) { 35 | $stmt instanceof Echo_ => true, 36 | $stmt instanceof Return_ => true, 37 | $stmt instanceof Assign => match (true) { 38 | $stmt->var instanceof Variable => false, 39 | $stmt->var instanceof PropertyFetch => true, 40 | $stmt->var instanceof ArrayDimFetch => true, 41 | default => false, 42 | }, 43 | $stmt instanceof InlineHTML => true, 44 | $stmt instanceof Print_ => true, 45 | $stmt instanceof Exit_ => true, 46 | $stmt instanceof FuncCall => true, 47 | $stmt instanceof MethodCall => true, 48 | $stmt instanceof New_ => true, 49 | $stmt instanceof PropertyFetch => true, 50 | $stmt instanceof ArrayDimFetch => true, 51 | $stmt instanceof Include_ => true, 52 | 53 | $stmt instanceof Use_ => true, 54 | $stmt instanceof UseUse => true, 55 | $stmt instanceof Namespace_ => true, 56 | 57 | $stmt instanceof Class_ => true, 58 | $stmt instanceof Function_ => true, 59 | $stmt instanceof ClassMethod => true, 60 | $stmt instanceof Property => true, 61 | 62 | // TODO 63 | default => false, 64 | }; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Visualization/AST/StandardPrettyPrinter.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class StandardPrettyPrinter extends Standard 25 | { 26 | /** @var SplObjectStorage */ 27 | private SplObjectStorage $seen; 28 | 29 | 30 | /** @param SplObjectStorage|null $seen */ 31 | public function __construct(SplObjectStorage $seen = null) 32 | { 33 | if ($seen === null) { 34 | /** @var SplObjectStorage */ 35 | $seen = new SplObjectStorage(); 36 | } 37 | $this->seen = $seen; 38 | 39 | parent::__construct(); 40 | } 41 | 42 | protected function p(Node $node, $parentFormatPreserved = false): string 43 | { 44 | $seen = $this->seen[$node] ?? null; 45 | if ($seen !== null) { 46 | return sprintf('[B%d.%d]', $seen[0]->getId(), $seen[1]); 47 | } 48 | 49 | $p = parent::p($node, $parentFormatPreserved); 50 | 51 | if (mb_strlen($p) > 100) { 52 | $p = mb_substr($p, 0, 100) . '...'; 53 | } 54 | 55 | return $p; 56 | } 57 | 58 | /** @param Comment[] $comments */ 59 | protected function pComments(array $comments): string { 60 | $formattedComments = []; 61 | 62 | foreach ($comments as $comment) { 63 | $formattedComments[] = str_replace("\n", $this->nl, $comment->getReformattedText()); 64 | } 65 | 66 | $comments = implode($this->nl, $formattedComments); 67 | 68 | if (mb_strlen($comments) > 30) { 69 | $comments = mb_substr($comments, 0, 30) . '... */'; 70 | } 71 | 72 | return $comments; 73 | } 74 | 75 | public function pStmt_Phi(Phi $node): string 76 | { 77 | return $this->p($node->var) . ' = Φ(' . implode(', ', array_map($this->p(...), $node->sources)) . ');'; 78 | } 79 | 80 | public function pExpr_Variable(Variable $node): string 81 | { 82 | if ($node->name instanceof Expr) { 83 | return '${' . $this->p($node->name) . '}'; 84 | } else { 85 | if ($node->name[0] === '.') { 86 | return $node->name; 87 | } 88 | $var = $node->getAttribute('ssa_var'); 89 | if ($var !== null) { 90 | assert(is_int($var)); 91 | $subscript = $this->toSubscript($var); 92 | } else { 93 | $subscript = ''; 94 | } 95 | return '$' . $node->name . $subscript; 96 | } 97 | } 98 | 99 | public function pExpr_Closure(Closure $node): string 100 | { 101 | return ($node->static ? 'static ' : '') 102 | . 'function ' . ($node->byRef ? '&' : '') 103 | . '(...)' 104 | . (!empty($node->uses) ? ' use(' . $this->pCommaSeparated($node->uses) . ')' : '') 105 | . (null !== $node->returnType ? ' : ' . $this->p($node->returnType) : '') 106 | . ' { ... }'; 107 | } 108 | 109 | public function pStmt_ClassMethod(ClassMethod $node): string { 110 | return $this->pModifiers($node->flags) 111 | . 'function ' . ($node->byRef ? '&' : '') . $node->name 112 | . '(...)' 113 | . (null !== $node->returnType ? ' : ' . $this->p($node->returnType) : '') 114 | . (null !== $node->stmts 115 | ? $this->nl . '{ ... }' 116 | : ';'); 117 | } 118 | 119 | public function pStmt_Function(Function_ $node): string { 120 | return 'function ' . ($node->byRef ? '&' : '') . $node->name 121 | . '(...)' 122 | . (null !== $node->returnType ? ' : ' . $this->p($node->returnType) : '') 123 | . $this->nl . '{ ... }'; 124 | } 125 | public function pStmt_Switch(Switch_ $node): string 126 | { 127 | return sprintf( 128 | 'switch (%s)', 129 | $this->p($node->cond), 130 | ); 131 | } 132 | 133 | /** @param string $afterClassToken */ 134 | protected function pClassCommon(Class_ $node, $afterClassToken): string { 135 | return $this->pModifiers($node->flags) 136 | . 'class' . $afterClassToken 137 | . (null !== $node->extends ? ' extends ' . $this->p($node->extends) : '') 138 | . (!empty($node->implements) ? ' implements ' . $this->pCommaSeparated($node->implements) : '') 139 | . $this->nl . '{ ... }'; 140 | } 141 | 142 | private function toSubscript(int $s): string 143 | { 144 | return str_replace( 145 | ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], 146 | ['₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉'], 147 | (string)$s, 148 | ); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Visualization/AST/TerminatorPrettyPrinter.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | class TerminatorPrettyPrinter extends Standard 33 | { 34 | /** @param SplObjectStorage $seen */ 35 | public function __construct( 36 | private SplObjectStorage $seen, 37 | private Standard $prettyPrinter, 38 | ) { 39 | parent::__construct(); 40 | } 41 | 42 | protected function p(Node $node, $parentFormatPreserved = false): string 43 | { 44 | $seen = $this->seen[$node] ?? null; 45 | if ($seen !== null) { 46 | return sprintf('[B%d.%d]', $seen[0]->getId(), $seen[1]); 47 | } 48 | 49 | return PrettyPrinterAbstract::p($node, $parentFormatPreserved); 50 | } 51 | 52 | public function pStmt_If(If_ $node): string 53 | { 54 | return sprintf('if (%s)', $this->prettyPrinter->p($node->cond)); 55 | } 56 | 57 | public function pStmt_For(For_ $node): string 58 | { 59 | $cond = end($node->cond); 60 | 61 | return sprintf( 62 | 'for (...; %s; ...)', 63 | $cond !== false ? $this->prettyPrinter->p($cond) : '', 64 | ); 65 | } 66 | 67 | public function pStmt_Foreach(Foreach_ $node): string 68 | { 69 | return sprintf( 70 | 'foreach (%s as ...)', 71 | $this->prettyPrinter->p($node->expr), 72 | ); 73 | } 74 | 75 | public function pStmt_While(While_ $node): string 76 | { 77 | return sprintf( 78 | 'while (%s)', 79 | $this->prettyPrinter->p($node->cond), 80 | ); 81 | } 82 | 83 | public function pStmt_Switch(Switch_ $node): string 84 | { 85 | return sprintf( 86 | 'switch (%s)', 87 | $this->prettyPrinter->p($node->cond), 88 | ); 89 | } 90 | 91 | public function pStmt_Case(Case_ $node): string 92 | { 93 | if ($node->cond === null) { 94 | return 'default:'; 95 | } 96 | 97 | return sprintf( 98 | 'case %s:', 99 | $this->prettyPrinter->p($node->cond), 100 | ); 101 | } 102 | 103 | public function pStmt_Do(Do_ $node): string 104 | { 105 | return sprintf( 106 | 'do ... while (%s)', 107 | $this->prettyPrinter->p($node->cond), 108 | ); 109 | } 110 | 111 | public function pExpr_Match(Match_ $node): string 112 | { 113 | return sprintf( 114 | 'match (%s)', 115 | $this->prettyPrinter->p($node->cond), 116 | ); 117 | } 118 | 119 | public function pMatchArm(MatchArm $node): string 120 | { 121 | return sprintf( 122 | 'match (...) { %s => ... }', 123 | '???', // TODO: print cond 124 | ); 125 | } 126 | 127 | public function pExpr_BinaryOp_BooleanAnd(BooleanAnd $node): string 128 | { 129 | return sprintf( 130 | '%s && ...', 131 | $this->prettyPrinter->p($node->left), 132 | ); 133 | } 134 | 135 | public function pExpr_BinaryOp_BooleanOr(BooleanOr $node): string 136 | { 137 | return sprintf( 138 | '%s || ...', 139 | $this->prettyPrinter->p($node->left), 140 | ); 141 | } 142 | 143 | public function pExpr_BinaryOp_LogicAnd(LogicalAnd $node): string 144 | { 145 | return sprintf( 146 | '%s and ...', 147 | $this->prettyPrinter->p($node->left), 148 | ); 149 | } 150 | 151 | public function pExpr_BinaryOp_LogicOr(LogicalOr $node): string 152 | { 153 | return sprintf( 154 | '%s or ...', 155 | $this->prettyPrinter->p($node->left), 156 | ); 157 | } 158 | 159 | public function pExpr_Ternary(Ternary $node): string 160 | { 161 | if ($node->if !== null) { 162 | return sprintf( 163 | '%s ? ... : ...', 164 | $this->prettyPrinter->p($node->cond), 165 | ); 166 | } else { 167 | return sprintf( 168 | '%s ?: ...', 169 | $this->prettyPrinter->p($node->cond), 170 | ); 171 | } 172 | } 173 | 174 | public function pStmt_TryCatch(TryCatch $node): string 175 | { 176 | return 'try (...)'; 177 | } 178 | 179 | public function pStmt_Catch(Catch_ $node): string 180 | { 181 | return 'catch (...)'; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/Visualization/CFG/Annotator/GraphvizDeadStmtAnnotator.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class GraphvizDeadStmtAnnotator implements GraphvizStmtAnnotator 14 | { 15 | /** @var SplObjectStorage */ 16 | private $deadStmts; 17 | 18 | public function __construct( 19 | DeadCodeAnalysisSSA $deadCodeAnalysisSSA, 20 | ) { 21 | $this->deadStmts = $deadCodeAnalysisSSA->getDeadStmts(); 22 | } 23 | 24 | public function getAnnotation(BBlock $block, Node $stmt, int $nodeIndex): ?string 25 | { 26 | if (!$this->deadStmts->contains($stmt)) { 27 | return null; 28 | } 29 | 30 | return "\u{1F480}"; // skull 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Visualization/CFG/Annotator/GraphvizLineNumberStmtAnnotator.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class GraphvizLineNumberStmtAnnotator implements GraphvizStmtAnnotator 12 | { 13 | private ?BBlock $prevBlock = null; 14 | 15 | private ?int $prevLine = null; 16 | 17 | public function getAnnotation(BBlock $block, Node $stmt, int $nodeIndex): ?string 18 | { 19 | if ($this->prevBlock === $block && $this->prevLine === $stmt->getLine()) { 20 | return null; 21 | } 22 | 23 | $this->prevBlock = $block; 24 | $this->prevLine = $stmt->getLine(); 25 | 26 | return (string) $stmt->getLine(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Visualization/CFG/Annotator/GraphvizStmtAnnotator.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface GraphvizStmtAnnotator 12 | { 13 | public function getAnnotation(BBlock $block, Node $stmt, int $nodeIndex): ?string; 14 | } 15 | -------------------------------------------------------------------------------- /src/Visualization/CFG/Annotator/GraphvizStmtClassAnnotator.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class GraphvizStmtClassAnnotator implements GraphvizStmtAnnotator 12 | { 13 | public function getAnnotation(BBlock $block, Node $stmt, int $nodeIndex): ?string 14 | { 15 | return addcslashes(get_class($stmt), '\\'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Visualization/CFG/GraphvizPrinter.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class GraphvizPrinter 19 | { 20 | private Graph $graph; 21 | 22 | /** 23 | * @param array $preStmtAnnotators 24 | * @param array $postStmtAnnotators 25 | */ 26 | public function __construct( 27 | private CFG $cfg, 28 | private array $preStmtAnnotators, 29 | private array $postStmtAnnotators, 30 | ) { 31 | $graph = new Graph(); 32 | 33 | /** @var SplObjectStorage */ 34 | $seen = new SplObjectStorage(); 35 | $prettyPrinter = new StandardPrettyPrinter($seen); 36 | $terminatorPrinter = new TerminatorPrettyPrinter($seen, $prettyPrinter); 37 | 38 | foreach ($this->cfg->getBBlocks() as $block) { 39 | $vertex = $graph->createVertex($block->getId()); 40 | $vertex->setAttribute( 41 | 'graphviz.label', 42 | GraphViz::raw('<'.$this->makeBlockLabel($block, $terminatorPrinter, $prettyPrinter, $seen).'>'), 43 | ); 44 | $vertex->setAttribute( 45 | 'graphviz.xlabel', 46 | sprintf('[B%d]', $block->getId()), 47 | ); 48 | $vertex->setAttribute('graphviz.shape', 'rectangle'); 49 | } 50 | 51 | foreach ($this->cfg->getBBlocks() as $block) { 52 | $successors = $block->getSuccessors(); 53 | if (isset($successors[1])) { 54 | $vertex = $graph->getVertex($block->getId()); 55 | $edge = $vertex->createEdgeTo($graph->getVertex($successors[0]->getId())); 56 | $edge->setAttribute('graphviz.label', 'true'); 57 | 58 | $vertex = $graph->getVertex($block->getId()); 59 | $edge = $vertex->createEdgeTo($graph->getVertex($successors[1]->getId())); 60 | $edge->setAttribute('graphviz.label', 'false'); 61 | 62 | continue; 63 | } 64 | if (isset($successors[0])) { 65 | $vertex = $graph->getVertex($block->getId()); 66 | $edge = $vertex->createEdgeTo($graph->getVertex($successors[0]->getId())); 67 | 68 | continue; 69 | } 70 | assert(count($successors) === 0); 71 | } 72 | 73 | $this->graph = $graph; 74 | } 75 | 76 | /** 77 | * @param string $format png, pdf, ... 78 | * 79 | * @see GraphViz::display() 80 | */ 81 | public function display(string $format = 'png'): void 82 | { 83 | $graphviz = new GraphViz(); 84 | $graphviz->setFormat($format); 85 | $graphviz->display($this->graph); 86 | } 87 | 88 | public function getGraph(): Graph 89 | { 90 | return $this->graph; 91 | } 92 | 93 | /** 94 | * @param SplObjectStorage $seen 95 | * 96 | * @return string 97 | */ 98 | private function makeBlockLabel( 99 | BBlock $block, 100 | TerminatorPrettyPrinter $terminatorPrinter, 101 | StandardPrettyPrinter $prettyPrinter, 102 | SplObjectStorage $seen, 103 | ): string { 104 | $label = []; 105 | 106 | if ($block === $this->cfg->getEntry()) { 107 | $label[] = '' . htmlspecialchars('') . ''; 108 | } 109 | 110 | if ($block === $this->cfg->getExit()) { 111 | $label[] = '' . htmlspecialchars('') . ''; 112 | } 113 | 114 | if ($block->getLabel() !== null) { 115 | $label[] = '' . htmlspecialchars($block->getLabel()) . ''; 116 | } 117 | 118 | /* 119 | $annotations = ($this->annotatePreBlock)($block); 120 | if ($annotations !== '') { 121 | $label[] = '' . $annotations . ''; 122 | } 123 | */ 124 | $stmts = $block->getStmts(); 125 | $terminator = $block->getTerminator(); 126 | if ($terminator !== null) { 127 | $stmts[] = $terminator; 128 | } 129 | if (count($stmts) > 0) { 130 | $table = ''; 131 | 132 | $table .= ''; 133 | foreach ($this->preStmtAnnotators as $key => $annotator) { 134 | $table .= ''; 135 | $table .= ''; 136 | } 137 | $table .= ''; 138 | foreach ($this->postStmtAnnotators as $key => $annotator) { 139 | $table .= ''; 140 | $table .= ''; 141 | } 142 | $table .= ''; 143 | 144 | foreach ($stmts as $i => $stmt) { 145 | $table .= ''; 146 | foreach ($this->preStmtAnnotators as $annotator) { 147 | $table .= ''; 148 | $table .= ''; 149 | } 150 | $table .= ''; 151 | if ($stmt === $block->getTerminator()) { 152 | $prettyPrinted = $terminatorPrinter->prettyPrint([$stmt]); 153 | } else { 154 | $prettyPrinted = $prettyPrinter->prettyPrint([$stmt]); 155 | } 156 | $seen[$stmt] = [$block, (int)$i]; 157 | $table .= ''; 158 | foreach ($this->postStmtAnnotators as $annotator) { 159 | $table .= ''; 160 | $table .= ''; 161 | } 162 | $table .= ''; 163 | } 164 | $table .= '
' . $key . ' StmtNo ' . $key . '
' . ($annotator->getAnnotation($block, $stmt, $i) ?? '') . ' ' . strval($stmt === $block->getTerminator() ? 'T' : $i) . ': ' . addcslashes(htmlspecialchars(trim($prettyPrinted)), '\\') . ' ' . ($annotator->getAnnotation($block, $stmt, $i) ?? '') . '
'; 165 | 166 | $label[] = $table; 167 | } 168 | 169 | /* 170 | $annotations = ($this->annotatePostBlock)($block); 171 | if ($annotations !== '') { 172 | $label[] = '' . $annotations . ''; 173 | } 174 | */ 175 | 176 | if (count($label) > 0) { 177 | $label = [ 178 | '', 179 | ...$label, 180 | '
', 181 | ]; 182 | } 183 | return implode('', $label); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/Visualization/CFG/GraphvizPrinterBuilder.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class GraphvizPrinterBuilder 12 | { 13 | /** @var GraphvizStmtAnnotator[] */ 14 | private array $preStmtAnnotators = []; 15 | 16 | /** @var GraphvizStmtAnnotator[] */ 17 | private array $postStmtAnnotators = []; 18 | 19 | public function __construct( 20 | private CFG $cfg, 21 | ) {} 22 | 23 | public static function create(CFG $cfg): self 24 | { 25 | return new self($cfg); 26 | } 27 | 28 | public function printer(): GraphvizPrinter 29 | { 30 | return new GraphvizPrinter( 31 | $this->cfg, 32 | $this->preStmtAnnotators, 33 | $this->postStmtAnnotators, 34 | ); 35 | } 36 | 37 | public function withPreStmtAnnotator(string $label, GraphvizStmtAnnotator $annotator): self 38 | { 39 | $clone = clone $this; 40 | $clone->preStmtAnnotators[$label] = $annotator; 41 | 42 | return $clone; 43 | } 44 | 45 | public function withPostStmtAnnotator(string $label, GraphvizStmtAnnotator $annotator): self 46 | { 47 | $clone = clone $this; 48 | $clone->postStmtAnnotators[$label] = $annotator; 49 | 50 | return $clone; 51 | } 52 | } 53 | --------------------------------------------------------------------------------