diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md index 156adf6c33f..de590b6250d 100644 --- a/docs/bootstrap-inventory.md +++ b/docs/bootstrap-inventory.md @@ -8,9 +8,9 @@ Regenerate: `php script/bootstrap-inventory.php` | Metric | Count | |--------|------:| -| PHP files on vm.php path | 212 | +| PHP files on vm.php path | 214 | | Source constructs flagged (blockers) | 10 | -| Source constructs flagged (warnings) | 553 | +| Source constructs flagged (warnings) | 570 | ## Compiler CFG gaps (`lib/Compiler.php`) @@ -162,7 +162,7 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: | `ext/types/strlen.php` | 0 | 1 | | `lib/AOT/Linker.php` | 8 | 1 | | `lib/Block.php` | 0 | 4 | -| `lib/Compiler.php` | 0 | 57 | +| `lib/Compiler.php` | 0 | 68 | | `lib/Frame.php` | 0 | 1 | | `lib/Func.php` | 0 | 1 | | `lib/Func/Internal.php` | 0 | 2 | @@ -199,6 +199,7 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: | `lib/JIT/Call.php` | 0 | 1 | | `lib/JIT/Call/Native.php` | 0 | 1 | | `lib/JIT/Call/Vararg.php` | 0 | 2 | +| `lib/JIT/CoalesceHelper.php` | 0 | 1 | | `lib/JIT/Context.php` | 0 | 15 | | `lib/JIT/HashTableHelper.php` | 0 | 1 | | `lib/JIT/Helper.php` | 0 | 7 | @@ -215,7 +216,8 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: | `lib/Lint/IncrementDetector.php` | 0 | 4 | | `lib/Lint/Issue.php` | 0 | 2 | | `lib/Lint/LintCompiler.php` | 0 | 6 | -| `lib/Lint/Linter.php` | 0 | 5 | +| `lib/Lint/Linter.php` | 0 | 6 | +| `lib/Lint/ListDestructuringDetector.php` | 0 | 4 | | `lib/Lint/UnsupportedRegistry.php` | 0 | 1 | | `lib/Module.php` | 0 | 1 | | `lib/ModuleAbstract.php` | 0 | 1 | @@ -1046,63 +1048,74 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: ### `lib/Compiler.php` **Warnings** (review for bootstrap subset): -- new Func\PHP (line 41) -- new Block (line 46) -- new OpCode (line 100) -- new Block (line 109) -- new OpCode (line 119) -- new Variable (line 134) -- new Operand\Temporary (line 135) -- new OpCode (line 157) -- new Operand\Literal (line 168) -- new OpCode (line 170) -- new OpCode (line 185) -- new OpCode (line 193) -- new OpCode (line 213) -- new OpCode (line 218) -- new OpCode (line 251) -- new OpCode (line 260) -- new OpCode (line 353) -- new OpCode (line 360) -- new OpCode (line 374) -- new OpCode (line 380) -- new OpCode (line 398) -- new OpCode (line 404) -- new OpCode (line 410) -- new OpCode (line 421) -- new OpCode (line 429) -- new OpCode (line 435) -- new OpCode (line 441) -- new OpCode (line 446) -- new OpCode (line 453) -- new OpCode (line 460) -- new OpCode (line 466) -- new OpCode (line 471) -- new OpCode (line 478) -- new OpCode (line 485) -- new OpCode (line 490) -- new OpCode (line 495) -- new OpCode (line 504) -- new OpCode (line 509) -- new OpCode (line 516) -- new OpCode (line 525) -- new OpCode (line 544) -- new Block (line 560) +- new Func\PHP (line 42) +- new Block (line 47) +- new OpCode (line 103) +- new Block (line 112) +- new OpCode (line 122) +- new Variable (line 137) +- new Operand\Temporary (line 138) +- new OpCode (line 160) +- new Operand\Literal (line 171) +- new OpCode (line 173) +- new OpCode (line 188) +- new OpCode (line 196) +- new OpCode (line 216) +- new OpCode (line 221) +- new OpCode (line 254) +- new OpCode (line 263) +- new OpCode (line 356) +- new OpCode (line 363) +- new OpCode (line 377) +- new OpCode (line 383) +- new OpCode (line 401) +- new OpCode (line 407) +- new OpCode (line 413) +- new OpCode (line 424) +- new OpCode (line 432) +- new OpCode (line 438) +- new OpCode (line 444) +- new OpCode (line 449) +- new OpCode (line 456) +- new OpCode (line 463) +- new OpCode (line 469) +- new OpCode (line 474) +- new OpCode (line 481) +- new OpCode (line 488) +- new OpCode (line 493) +- new OpCode (line 498) +- new OpCode (line 507) +- new OpCode (line 512) +- new OpCode (line 519) +- new OpCode (line 528) +- new OpCode (line 547) +- new Block (line 559) - new Block (line 563) -- new OpCode (line 566) -- new OpCode (line 572) -- new OpCode (line 586) -- new Block (line 593) -- new OpCode (line 596) -- new OpCode (line 606) -- new Operand\Temporary (line 616) -- new Variable (line 624) -- new Operand\Temporary (line 626) -- new Variable (line 687) -- new OpCode (line 717) -- new OpCode (line 723) -- new OpCode (line 727) -- 25 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler +- new OpCode (line 567) +- new Block (line 574) +- new OpCode (line 582) +- new OpCode (line 589) +- new OpCode (line 597) +- new OpCode (line 603) +- new OpCode (line 611) +- new OpCode (line 614) +- new OpCode (line 620) +- new Block (line 674) +- new Block (line 677) +- new OpCode (line 680) +- new OpCode (line 686) +- new OpCode (line 700) +- new Block (line 707) +- new OpCode (line 710) +- new OpCode (line 720) +- new Temporary (line 730) +- new Variable (line 740) +- new Operand\Temporary (line 742) +- new Variable (line 803) +- new OpCode (line 833) +- new OpCode (line 839) +- new OpCode (line 843) +- 28 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler ### `lib/Frame.php` @@ -1142,13 +1155,13 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: - new Variable (line 139) - new JIT\Call\Vararg (line 146) - new JIT\Call\Native (line 149) -- new ext\standard\boolval (line 249) -- new Variable (line 413) -- new Variable (line 734) -- new Operand\Literal (line 804) -- new Operand\Literal (line 808) -- new Operand\Literal (line 812) -- 11 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler +- new ext\standard\boolval (line 263) +- new Variable (line 427) +- new Variable (line 773) +- new Operand\Literal (line 843) +- new Operand\Literal (line 847) +- new Operand\Literal (line 851) +- 12 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler ### `lib/JIT/Analyzer.php` @@ -1312,6 +1325,11 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: - 1 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler - 1 closure(s) +### `lib/JIT/CoalesceHelper.php` + +**Warnings** (review for bootstrap subset): +- 1 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler + ### `lib/JIT/Context.php` **Warnings** (review for bootstrap subset): @@ -1450,11 +1468,20 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: **Warnings** (review for bootstrap subset): - new Runtime (line 24) -- new IncrementDetector (line 44) -- new State (line 86) -- new LintCompiler (line 101) +- new IncrementDetector (line 45) +- new ListDestructuringDetector (line 46) +- new State (line 89) +- new LintCompiler (line 104) - 9 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler +### `lib/Lint/ListDestructuringDetector.php` + +**Warnings** (review for bootstrap subset): +- new ParserFactory (line 27) +- new NodeTraverser (line 54) +- new Issue (line 61) +- 2 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler + ### `lib/Lint/UnsupportedRegistry.php` **Warnings** (review for bootstrap subset): @@ -1510,10 +1537,10 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: **Warnings** (review for bootstrap subset): - new Variable (line 64) - new Variable (line 69) -- new Func\PHP (line 217) -- new ClassEntry (line 267) -- new ObjectEntry (line 279) -- new VM\ClassProperty (line 369) +- new Func\PHP (line 224) +- new ClassEntry (line 274) +- new ObjectEntry (line 286) +- new VM\ClassProperty (line 376) - 3 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler ### `lib/VM/ClassEntry.php` diff --git a/docs/unsupported-syntax.md b/docs/unsupported-syntax.md index 00ddb12e942..123e2232893 100644 --- a/docs/unsupported-syntax.md +++ b/docs/unsupported-syntax.md @@ -19,7 +19,6 @@ Some constructs (for example destructuring assigns and prefix/postfix `++`/`--`) | CFG kind | Tracking | |----------|----------| | `Stmt_Foreach` | [#53](https://github.com/PurHur/php-compiler/issues/53) | -| `Expr_BinaryOp_Coalesce` (`??`) | [#99](https://github.com/PurHur/php-compiler/issues/99) | | `Expr_Throw`, `Stmt_Try`, `Stmt_Catch`, `Stmt_Finally` | [#195](https://github.com/PurHur/php-compiler/issues/195) | | `Expr_Ternary` (`?:`) | [#114](https://github.com/PurHur/php-compiler/issues/114) | | `Expr_AssignOp_*`, `Expr_BinaryOp_ShiftLeft` / `ShiftRight` (compound assign) | [#136](https://github.com/PurHur/php-compiler/issues/136) | diff --git a/lib/Compiler.php b/lib/Compiler.php index 2eeef27aef6..42027973159 100755 --- a/lib/Compiler.php +++ b/lib/Compiler.php @@ -14,6 +14,7 @@ use PHPCfg\Op; use PHPCfg\Block as CfgBlock; use PHPCfg\Operand; +use PHPCfg\Operand\Temporary; use PHPCfg\Script; use PHPTypes\Type; use PHPCompiler\VM\Variable; @@ -81,6 +82,8 @@ protected function compileOps(array $ops, Block $block): void { default: if ($child instanceof Op\Expr\Isset_ && count($child->vars) > 1) { $block = $this->compileIssetMulti($child, $block); + } elseif ($child instanceof Op\Expr\BinaryOp\Coalesce) { + $block = $this->compileCoalesce($child, $block); } else { $this->compileOp($child, $block); } @@ -549,6 +552,117 @@ protected function compileIsset(Op\Expr\Isset_ $expr, Block $block): array )]; } + protected function compileCoalesce(Op\Expr\BinaryOp\Coalesce $expr, Block $block): Block + { + $resultSlot = $this->compileOperand($expr->result, $block, false); + + $endBlock = new Block($block->orig); + $endBlock->inheritUndefinedLocals = true; + $endBlock->inheritScopeFrom($block); + + $rightBlock = new Block($block->orig); + $rightBlock->inheritUndefinedLocals = true; + $rightBlock->inheritScopeFrom($block); + $rightSlot = $this->compileOperand($expr->right, $rightBlock, true); + $rightBlock->addOpCode(new OpCode( + OpCode::TYPE_ASSIGN, + $resultSlot, + $resultSlot, + $rightSlot + )); + + $leftBlock = new Block($block->orig); + $leftBlock->inheritUndefinedLocals = true; + $leftBlock->inheritScopeFrom($block); + + $checkSlot = $this->compileBoolTemporary($block); + $issetTarget = $this->resolveCoalesceIssetTarget($expr->left, $block); + if (null !== $issetTarget) { + [$containerSlot, $dimSlot] = $issetTarget; + $block->addOpCode(new OpCode( + OpCode::TYPE_ISSET, + $checkSlot, + $containerSlot, + $dimSlot + )); + $leftSlot = $this->compileOperand($expr->left, $leftBlock, true); + $leftBlock->addOpCode(new OpCode( + OpCode::TYPE_ASSIGN, + $resultSlot, + $resultSlot, + $leftSlot + )); + } else { + $leftSlot = $this->compileOperand($expr->left, $block, true); + $block->addOpCode(new OpCode( + OpCode::TYPE_ASSIGN, + $resultSlot, + $resultSlot, + $leftSlot + )); + $block->addOpCode(new OpCode( + OpCode::TYPE_ISSET, + $checkSlot, + $leftSlot, + null + )); + } + + $leftJump = new OpCode(OpCode::TYPE_JUMP); + $leftJump->block1 = $endBlock; + $leftBlock->addOpCode($leftJump); + $rightJump = new OpCode(OpCode::TYPE_JUMP); + $rightJump->block1 = $endBlock; + $rightBlock->addOpCode($rightJump); + $endBlock->parents[] = $leftBlock; + $endBlock->parents[] = $rightBlock; + + $coalesceOp = new OpCode( + OpCode::TYPE_COALESCE, + $resultSlot, + $checkSlot + ); + $coalesceOp->block1 = $leftBlock; + $coalesceOp->block2 = $rightBlock; + $coalesceOp->block3 = $endBlock; + $block->addOpCode($coalesceOp); + + return $endBlock; + } + + /** + * @return ?array{0: int, 1: ?int} + */ + protected function resolveCoalesceIssetTarget(Operand $operand, Block $block): ?array + { + if (null !== $this->unwrapArrayDimFetch($operand)) { + return $this->resolveIssetTarget($operand, $block); + } + if (null !== $this->unwrapVariableOperand($operand)) { + return $this->resolveIssetTarget($operand, $block); + } + + return null; + } + + protected function unwrapVariableOperand(Operand $operand): ?Operand\Variable + { + while ($operand instanceof Temporary) { + if ($operand->original instanceof Operand\Variable) { + return $operand->original; + } + if (null === $operand->original) { + return null; + } + $operand = $operand->original; + } + if ($operand instanceof Operand\Variable) { + return $operand; + } + + return null; + } + /** * isset($a, $b, …) with short-circuit evaluation (PHP semantics). * Returns the block where compilation should continue. @@ -613,8 +727,10 @@ protected function compileIssetMulti(Op\Expr\Isset_ $expr, Block $block): Block protected function compileBoolTemporary(Block $block): int { - $operand = new Operand\Temporary; + $operand = new Temporary; $operand->type = Type::bool(); + // JIT assignOperandValue skips operands with empty usages (#99 coalesce branches). + $operand->usages[] = $operand; return $block->getVarSlot($operand, false); } diff --git a/lib/JIT.php b/lib/JIT.php index 0c8bc7962bc..9fa18ae6c48 100644 --- a/lib/JIT.php +++ b/lib/JIT.php @@ -156,10 +156,24 @@ private function compileBlock(Block $block, ?string $funcName = null): PHPLLVM\V } return $func; } + + public function compileSubBlock( + PHPLLVM\Value $func, + Block $block, + Variable ...$args + ): PHPLLVM\BasicBlock { + $limit = $block->nOpCodes; + if ($limit > 0 && OpCode::TYPE_JUMP === $block->opCodes[$limit - 1]->type) { + --$limit; + } + + return $this->compileBlockInternal($func, $block, $limit, ...$args); + } private function compileBlockInternal( PHPLLVM\Value $func, Block $block, + ?int $limit = null, Variable ...$args ): PHPLLVM\BasicBlock { if ($this->context->scope->blockStorage->contains($block)) { @@ -175,7 +189,7 @@ private function compileBlockInternal( $this->context->makeVariableFromOp($func, $basicBlock, $block, $operand); } - for ($i = 0, $length = count($block->opCodes); $i < $length; $i++) { + for ($i = 0, $length = null !== $limit ? $limit : count($block->opCodes); $i < $length; $i++) { $op = $block->opCodes[$i]; switch ($op->type) { case OpCode::TYPE_ARG_RECV: @@ -478,6 +492,30 @@ private function compileBlockInternal( $builder->positionAtEnd($branchBlock); $this->context->freeDeadVariables($func, $branchBlock, $block); $builder->branch($newBlock); + return $origBasicBlock; + case OpCode::TYPE_COALESCE: + $branchBlock = $builder->getInsertBlock(); + $builder->positionAtEnd($branchBlock); + $condition = $this->context->castToBool( + $this->context->helper->loadValue($this->context->getVariableFromOp($block->getOperand($op->arg2))) + ); + $leftBb = JIT\CoalesceHelper::compileBranch($this, $func, $op->block1); + $rightBb = JIT\CoalesceHelper::compileBranch($this, $func, $op->block2); + $builder->positionAtEnd($branchBlock); + $this->context->freeDeadVariables($func, $branchBlock, $block); + $builder->branchIf($condition, $leftBb, $rightBb); + if (null !== $op->block3) { + $mergeBb = JIT\BasicBlockHelper::append($this->context, 'coalesce_merge'); + $builder->positionAtEnd($leftBb); + $builder->branch($mergeBb); + $builder->positionAtEnd($rightBb); + $builder->branch($mergeBb); + $builder->positionAtEnd($mergeBb); + $this->context->scope->blockStorage[$op->block3] = $mergeBb; + + return $this->compileBlockInternal($func, $op->block3, ...$args); + } + return $origBasicBlock; case OpCode::TYPE_JUMPIF: $branchBlock = $builder->getInsertBlock(); @@ -572,7 +610,8 @@ private function compileBlockInternal( throw new \LogicException("Unknown JIT opcode: ". $op->getType()); } } - throw new \LogicException("Reached the end of the loop, this shouldn't happen..."); + + return $builder->getInsertBlock(); } private function compileClass(?Block $block, int $classId) { diff --git a/lib/JIT/CoalesceHelper.php b/lib/JIT/CoalesceHelper.php new file mode 100644 index 00000000000..a6a3d802c25 --- /dev/null +++ b/lib/JIT/CoalesceHelper.php @@ -0,0 +1,24 @@ +compileSubBlock($func, $branchBlock); + } +} diff --git a/lib/Lint/UnsupportedRegistry.php b/lib/Lint/UnsupportedRegistry.php index 804e2885a4a..f4efb1125b9 100644 --- a/lib/Lint/UnsupportedRegistry.php +++ b/lib/Lint/UnsupportedRegistry.php @@ -20,8 +20,6 @@ final class UnsupportedRegistry 'Iterator_Key' => 53, 'Iterator_Current' => 53, 'Iterator_Value' => 53, - 'Expr_BinaryOp_Coalesce' => 99, - 'Expr_Coalesce' => 99, 'Expr_Throw' => 195, 'Expr_New' => 136, 'Stmt_Try' => 195, diff --git a/lib/OpCode.php b/lib/OpCode.php index 51223835fd9..efd61a401fa 100755 --- a/lib/OpCode.php +++ b/lib/OpCode.php @@ -71,6 +71,7 @@ class OpCode { const TYPE_NOT_EQUAL = 59; const TYPE_NOT_IDENTICAL = 60; const TYPE_SPACESHIP = 61; + const TYPE_COALESCE = 62; public int $type; public ?int $arg1; @@ -78,6 +79,7 @@ class OpCode { public ?int $arg3; public ?Block $block1 = null; public ?Block $block2 = null; + public ?Block $block3 = null; public function __construct(int $type, ?int $arg1 = null, ?int $arg2 = null, ?int $arg3 = null) { $this->type = $type; diff --git a/lib/VM.php b/lib/VM.php index 676eeb4e4cf..e140ce24c1b 100755 --- a/lib/VM.php +++ b/lib/VM.php @@ -156,6 +156,13 @@ public function run(Block $block): int { echo $frame->scope[$op->arg2]->toString(); $frame->scope[$op->arg1]->int(1); break; + case OpCode::TYPE_COALESCE: + $takeLeft = $frame->scope[$op->arg2]->toBool(); + $frame = ($takeLeft ? $op->block1 : $op->block2)->getFrame( + $this->context, + $frame + ); + goto restart; case OpCode::TYPE_EXIT: $exitArg = null; if (null !== $op->arg2) { diff --git a/patches/php-types-binaryop-coalesce.patch b/patches/php-types-binaryop-coalesce.patch new file mode 100644 index 00000000000..63d0a6d2fb0 --- /dev/null +++ b/patches/php-types-binaryop-coalesce.patch @@ -0,0 +1,21 @@ +--- vendor/ircmaxell/php-types/lib/PHPTypes/TypeReconstructor.php ++++ vendor/ircmaxell/php-types/lib/PHPTypes/TypeReconstructor.php +@@ -198,6 +198,17 @@ + } + + return false; ++ case 'Expr_BinaryOp_Coalesce': ++ if ($resolved->contains($op->left) && $resolved->contains($op->right)) { ++ $types = [$resolved[$op->left], $resolved[$op->right]]; ++ $merged = $this->computeMergedType($types); ++ if ($merged) { ++ return [$merged]; ++ } ++ ++ return [Type::mixed()]; ++ } ++ ++ return false; + case 'Expr_BinaryOp_Concat': + case 'Expr_Cast_String': + case 'Expr_ConcatList': diff --git a/script/apply-patches.sh b/script/apply-patches.sh index 92726af948c..4ddceea6f57 100755 --- a/script/apply-patches.sh +++ b/script/apply-patches.sh @@ -40,6 +40,7 @@ fi if [[ -d "$ROOT/vendor/ircmaxell/php-types" ]]; then apply_patch "$PATCH_DIR/php-types-binaryop-pow.patch" + apply_patch "$PATCH_DIR/php-types-binaryop-coalesce.patch" apply_patch "$PATCH_DIR/php-types-binaryop-spaceship.patch" apply_patch "$PATCH_DIR/php-types-str-bool-fns.patch" apply_patch "$PATCH_DIR/php-types-dollars-brace.patch" diff --git a/test/compliance/cases/language/coalesce.phpt b/test/compliance/cases/language/coalesce.phpt new file mode 100644 index 00000000000..3c8ebf0ce8b --- /dev/null +++ b/test/compliance/cases/language/coalesce.phpt @@ -0,0 +1,31 @@ +--TEST-- +Language: null coalescing operator (??) +--FILE-- + 'home']; +echo $items['page'] ?? 'index', "\n"; +echo $items['missing'] ?? 'index', "\n"; + +echo 0 ?? 'zero', "\n"; +echo '' ?? 'empty', "\n"; +--EXPECT-- +Guest +Alice +home +index +0 + +--FILE-- +assertMatchesRegularExpression('/line \d+/', $exit['stdout']); } - public function testLintCoalesceReportsIssue99(): void + public function testLintCoalesceAccepted(): void { $code = 'runLint(['-r', $code]); - $this->assertSame(1, $exit['code']); - $this->assertStringContainsString('#99', $exit['stdout']); + $this->assertSame(0, $exit['code']); } public function testLintShiftAssignReportsIssue136(): void