diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md index a13b9a26a12..b34e59a9edb 100644 --- a/docs/bootstrap-inventory.md +++ b/docs/bootstrap-inventory.md @@ -10,7 +10,7 @@ Regenerate: `php script/bootstrap-inventory.php` |--------|------:| | PHP files on vm.php path | 299 | | Source constructs flagged (blockers) | 10 | -| Source constructs flagged (warnings) | 786 | +| Source constructs flagged (warnings) | 787 | ## Compiler CFG gaps (`lib/Compiler.php`) @@ -226,7 +226,7 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: | `lib/AOT/ProjectGraph.php` | 0 | 4 | | `lib/Block.php` | 0 | 4 | | `lib/Cli/PhpcInit.php` | 0 | 1 | -| `lib/Compiler.php` | 0 | 93 | +| `lib/Compiler.php` | 0 | 94 | | `lib/Doctor.php` | 0 | 1 | | `lib/Frame.php` | 0 | 1 | | `lib/Func.php` | 0 | 1 | @@ -1503,9 +1503,9 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: ### `lib/Block.php` **Warnings** (review for bootstrap subset): -- new Variable (line 202) -- new Frame (line 227) -- new Variable (line 250) +- new Variable (line 216) +- new Frame (line 241) +- new Variable (line 264) - 11 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler ### `lib/Cli/PhpcInit.php` @@ -1518,96 +1518,97 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: **Warnings** (review for bootstrap subset): - new Func\PHP (line 47) - new Block (line 52) -- new OpCode (line 143) -- new Block (line 152) -- new OpCode (line 165) -- new Operand\Literal (line 176) -- new OpCode (line 178) -- new Variable (line 193) -- new Operand\Temporary (line 194) -- new OpCode (line 225) -- new Operand\Literal (line 237) -- new OpCode (line 239) -- new OpCode (line 254) -- new OpCode (line 262) -- new OpCode (line 282) -- new OpCode (line 287) -- new OpCode (line 320) -- new OpCode (line 329) -- new OpCode (line 426) -- new OpCode (line 433) -- new OpCode (line 447) -- new OpCode (line 453) -- new OpCode (line 464) +- new Block (line 67) +- new OpCode (line 160) +- new Block (line 169) +- new OpCode (line 182) +- new Operand\Literal (line 193) +- new OpCode (line 195) +- new Variable (line 210) +- new Operand\Temporary (line 211) +- new OpCode (line 242) +- new Operand\Literal (line 254) +- new OpCode (line 256) +- new OpCode (line 271) +- new OpCode (line 279) +- new OpCode (line 299) +- new OpCode (line 303) +- new OpCode (line 334) +- new OpCode (line 342) +- new OpCode (line 438) +- new OpCode (line 445) +- new OpCode (line 459) +- new OpCode (line 465) - new OpCode (line 476) -- new OpCode (line 482) -- new OpCode (line 495) -- new OpCode (line 506) -- new OpCode (line 514) -- new OpCode (line 520) +- new OpCode (line 488) +- new OpCode (line 494) +- new OpCode (line 507) +- new OpCode (line 518) - new OpCode (line 526) -- new OpCode (line 531) +- new OpCode (line 532) - new OpCode (line 538) -- new OpCode (line 545) -- new OpCode (line 551) -- new OpCode (line 556) +- new OpCode (line 543) +- new OpCode (line 550) +- new OpCode (line 557) - new OpCode (line 563) -- new OpCode (line 570) +- new OpCode (line 568) - new OpCode (line 575) -- new OpCode (line 581) -- new OpCode (line 588) -- new OpCode (line 594) -- new OpCode (line 599) +- new OpCode (line 582) +- new OpCode (line 587) +- new OpCode (line 593) +- new OpCode (line 600) - new OpCode (line 606) -- new OpCode (line 615) -- new OpCode (line 620) +- new OpCode (line 611) +- new OpCode (line 618) - new OpCode (line 627) -- new OpCode (line 640) -- new OpCode (line 646) +- new OpCode (line 632) +- new OpCode (line 639) - new OpCode (line 652) -- new OpCode (line 671) -- new Operand\Literal (line 694) -- new OpCode (line 697) -- new OpCode (line 705) -- new Block (line 720) -- new Block (line 724) -- new OpCode (line 728) -- new Block (line 735) -- new OpCode (line 746) -- new OpCode (line 755) -- new OpCode (line 763) -- new OpCode (line 772) -- new OpCode (line 778) -- new OpCode (line 786) -- new OpCode (line 789) -- new OpCode (line 795) -- new OpCode (line 813) -- new OpCode (line 828) -- new Block (line 847) -- new Block (line 851) -- new Operand\Literal (line 854) -- new OpCode (line 857) -- new OpCode (line 863) -- new Block (line 867) -- new OpCode (line 871) -- new OpCode (line 877) -- new Block (line 961) -- new Block (line 964) -- new OpCode (line 967) -- new OpCode (line 973) -- new OpCode (line 987) -- new Block (line 994) -- new OpCode (line 997) -- new OpCode (line 1007) -- new Temporary (line 1017) -- new Variable (line 1027) -- new Operand\Temporary (line 1029) -- new Variable (line 1122) -- new OpCode (line 1154) -- new OpCode (line 1160) -- new OpCode (line 1164) -- new OpCode (line 1169) -- 36 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler +- new OpCode (line 658) +- new OpCode (line 664) +- new OpCode (line 683) +- new Operand\Literal (line 706) +- new OpCode (line 709) +- new OpCode (line 717) +- new Block (line 732) +- new Block (line 736) +- new OpCode (line 740) +- new Block (line 747) +- new OpCode (line 758) +- new OpCode (line 767) +- new OpCode (line 775) +- new OpCode (line 784) +- new OpCode (line 790) +- new OpCode (line 798) +- new OpCode (line 801) +- new OpCode (line 807) +- new OpCode (line 825) +- new OpCode (line 840) +- new Block (line 859) +- new Block (line 863) +- new Operand\Literal (line 866) +- new OpCode (line 869) +- new OpCode (line 875) +- new Block (line 879) +- new OpCode (line 883) +- new OpCode (line 889) +- new Block (line 973) +- new Block (line 976) +- new OpCode (line 979) +- new OpCode (line 985) +- new OpCode (line 999) +- new Block (line 1006) +- new OpCode (line 1009) +- new OpCode (line 1019) +- new Temporary (line 1029) +- new Variable (line 1039) +- new Operand\Temporary (line 1041) +- new Variable (line 1134) +- new OpCode (line 1166) +- new OpCode (line 1172) +- new OpCode (line 1176) +- new OpCode (line 1181) +- 37 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler - 1 closure(s) ### `lib/Doctor.php` diff --git a/lib/Block.php b/lib/Block.php index bd9ee0219ec..443e7b600e9 100755 --- a/lib/Block.php +++ b/lib/Block.php @@ -117,15 +117,20 @@ public function addOpCode(OpCode ...$ops): void { } public function findSlot(Operand $op, Frame $frame): ?Variable { + $byName = self::findVariableInParentFrames($op, $frame); + if (null !== $byName) { + return $byName; + } if (!$this->scope->contains($op)) { - // check PHI vars if (!is_null($frame->parent)) { return $frame->parent->block->findSlot($op, $frame->parent); } + return null; } $idx = $this->scope[$op]; - return $frame->scope[$idx]; + + return $frame->scope[$idx] ?? null; } public function slotIndexForVariableName(string $name): ?int @@ -149,6 +154,9 @@ private static function findVariableInParentFrames(Operand $op, Frame $frame): ? return null; } for ($f = $frame; null !== $f; $f = $f->parent) { + if ('this' === $name && !empty($f->calledArgs)) { + return $f->calledArgs[0]; + } if (null === $f->block) { continue; } @@ -167,9 +175,15 @@ public function getFrame(Context $context, ?Frame $frame = null): Frame { $scopeSize = $this->scope->count(); foreach ($this->scope as $op) { $pos = $this->scope[$op]; - if (null !== $frame && 'this' === self::resolveVariableName($op) && !empty($frame->callArgs)) { - $scope[$pos] = $frame->callArgs[0]; - continue; + if (null !== $frame && 'this' === self::resolveVariableName($op)) { + if (!empty($frame->callArgs)) { + $scope[$pos] = $frame->callArgs[0]; + continue; + } + if (!empty($frame->calledArgs)) { + $scope[$pos] = $frame->calledArgs[0]; + continue; + } } if (isset($this->constants[$pos])) { diff --git a/lib/Compiler.php b/lib/Compiler.php index 851daaae29f..a47472584db 100755 --- a/lib/Compiler.php +++ b/lib/Compiler.php @@ -59,6 +59,23 @@ protected function compileCfgBlock(CfgBlock $block, array $params = []): Block { return $this->seen[$block]; } + /** + * CFG branch target within the current function: inherit parent locals ($this, params). + */ + protected function compileCfgBranch(CfgBlock $block, Block $parent): Block { + if (!$this->seen->contains($block)) { + $this->seen[$block] = $new = new Block($block); + $new->inheritScopeFrom($parent); + $this->compileBlock($new); + } else { + $this->seen[$block]->inheritScopeFrom($parent); + } + $child = $this->seen[$block]; + $child->parents[] = $parent; + + return $child; + } + protected function compileBlock(Block $block) { $this->compileOps($block->orig->children, $block); } @@ -280,15 +297,12 @@ protected function compileOp(Op $op, Block $block) { protected function compileStmt(Op\Stmt $stmt, Block $block) { if ($stmt instanceof Op\Stmt\Jump) { $op = new OpCode(OpCode::TYPE_JUMP); - $op->block1 = $this->compileCfgBlock($stmt->target); - $op->block1->parents[] = $block; + $op->block1 = $this->compileCfgBranch($stmt->target, $block); $block->addOpCode($op); } elseif ($stmt instanceof Op\Stmt\JumpIf) { $op = new OpCode(OpCode::TYPE_JUMPIF, $this->compileOperand($stmt->cond, $block, true)); - $op->block1 = $this->compileCfgBlock($stmt->if); - $op->block2 = $this->compileCfgBlock($stmt->else); - $op->block1->parents[] = $block; - $op->block2->parents[] = $block; + $op->block1 = $this->compileCfgBranch($stmt->if, $block); + $op->block2 = $this->compileCfgBranch($stmt->else, $block); $block->addOpCode($op); } elseif ($stmt instanceof Op\Stmt\Switch_) { $canBeSwitch = true; @@ -322,13 +336,11 @@ protected function compileSwitchStmt(Op\Stmt\Switch_ $switch, Block $block): voi $op, $this->compileOperand($case, $block, true) ); - $caseOp->block1 = $this->compileCfgBlock($switch->targets[$key]); - $caseOp->block1->parents[] = $block; + $caseOp->block1 = $this->compileCfgBranch($switch->targets[$key], $block); $block->addOpCode($caseOp); } $defaultOp = new OpCode(OpCode::TYPE_JUMP); - $defaultOp->block1 = $this->compileCfgBlock($switch->default); - $defaultOp->block1->parents[] = $block; + $defaultOp->block1 = $this->compileCfgBranch($switch->default, $block); $block->addOpCode($defaultOp); } diff --git a/test/compliance/cases/language/switch_method_call.phpt b/test/compliance/cases/language/switch_method_call.phpt new file mode 100644 index 00000000000..ac58d6175ac --- /dev/null +++ b/test/compliance/cases/language/switch_method_call.phpt @@ -0,0 +1,23 @@ +--TEST-- +switch case can call private methods on $this (MiniWebApp Router dispatch) +--FILE-- +renderHome(); + break; + default: + echo "no\n"; + } + } + + private function renderHome(): void { + echo "home\n"; + } +} + +(new R())->dispatch('home'); +--EXPECT-- +home diff --git a/test/unit/PhpcLintProjectTest.php b/test/unit/PhpcLintProjectTest.php index 04bfded00b5..976307d6547 100644 --- a/test/unit/PhpcLintProjectTest.php +++ b/test/unit/PhpcLintProjectTest.php @@ -66,16 +66,16 @@ public function testDirRelativeIncludeIsFollowed(): void /** * @see https://github.com/PurHur/php-compiler/issues/462 */ - public function testLintMiniWebAppHasNoDynamicIncludeWarnings(): void + public function testLintMiniWebAppExitsZero(): void { $tree = dirname(__DIR__, 2).'/examples/003-MiniWebApp'; $exit = $this->runLint(['--all', $tree]); + $this->assertSame(0, $exit['code'], $exit['stderr']."\n".$exit['stdout']); $this->assertStringNotContainsString( 'dynamic include/require', $exit['stderr'], $exit['stderr']."\n".$exit['stdout'] ); - $this->assertStringContainsString('ClassMethod', $exit['stderr']."\n".$exit['stdout']); } public function testDynamicIncludeEmitsWarning(): void diff --git a/test/unit/SwitchMethodCallTest.php b/test/unit/SwitchMethodCallTest.php new file mode 100644 index 00000000000..35db07eb37c --- /dev/null +++ b/test/unit/SwitchMethodCallTest.php @@ -0,0 +1,80 @@ +renderHome(); + break; + } + } + private function renderHome(): void { + echo "home\n"; + } +} +(new R())->dispatch('home'); +PHP; + $tmp = tempnam(sys_get_temp_dir(), 'sw_meth_'); + $this->assertNotFalse($tmp); + $path = $tmp.'.php'; + rename($tmp, $path); + file_put_contents($path, $code); + try { + $vm = realpath(__DIR__.'/../../bin/vm.php'); + $this->assertNotFalse($vm); + $cmd = array_merge( + [PHP_BINARY], + $this->extensionFlags(), + [$vm, $path] + ); + $proc = proc_open($cmd, [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, dirname($vm)); + $this->assertIsResource($proc); + fclose($pipes[0]); + $stdout = stream_get_contents($pipes[1]); + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + $code = proc_close($proc); + $this->assertSame(0, $code, $stderr !== false ? $stderr : ''); + $this->assertStringContainsString('home', $stdout !== false ? $stdout : ''); + } finally { + @unlink($path); + } + } + + /** + * @return list + */ + private function extensionFlags(): array + { + $flags = []; + $extDir = getenv('PHP_COMPILER_EXT_DIR') ?: '/usr/lib/php/20220829'; + if (!is_dir($extDir)) { + return $flags; + } + foreach (['tokenizer', 'mbstring', 'dom', 'xml', 'xmlwriter', 'ffi', 'posix', 'phar'] as $ext) { + $so = $extDir.'/'.$ext.'.so'; + if (is_file($so)) { + $flags[] = '-d'; + $flags[] = 'extension='.$so; + } + } + + return $flags; + } +}