From a702765af617f671078288fb1563457278cf4570 Mon Sep 17 00:00:00 2001 From: PurHur Date: Sat, 30 May 2026 13:42:31 +0000 Subject: [PATCH] Language: throw expressions in ?:, ??, && contexts (#3802) Fix duplicate New lowering before Throw, allocate throw objects outside ?: merge phi slots, and skip unreachable CFG ops after throw-expr. Adds compliance coverage and capability-syntax entry for PHP 8.0 throw expressions. php-src: Zend/zend_compile.c zend_compile_throw(), zend_language_parser.y Co-authored-by: Cursor --- lib/Block.php | 9 + lib/Compiler.php | 192 +++++++++++++++++- script/capability-syntax-lib.php | 13 ++ .../cases/language/throw_expression.phpt | 40 ++++ 4 files changed, 244 insertions(+), 10 deletions(-) create mode 100644 test/compliance/cases/language/throw_expression.phpt diff --git a/lib/Block.php b/lib/Block.php index 95a7e6f637e..ef9a1b8fad8 100755 --- a/lib/Block.php +++ b/lib/Block.php @@ -259,6 +259,15 @@ public function getVarSlot(Operand $operand, bool $isRead): int { return $this->scope[$operand]; } + /** Bind operand to a fresh slot (?: throw arm must not alias merge phi slot, #3802). */ + public function forceFreshVarSlot(Operand $operand): int + { + $slot = $this->nextScopeSlot(); + $this->scope[$operand] = $slot; + + return $slot; + } + /** Next unused scope slot (SplObjectStorage::count() can collide after inheritScopeFrom, #1058). */ private function nextScopeSlot(): int { diff --git a/lib/Compiler.php b/lib/Compiler.php index 9b422af257d..b91c7524a33 100755 --- a/lib/Compiler.php +++ b/lib/Compiler.php @@ -603,6 +603,31 @@ private function mergeEchoSlot(Block $merge): ?int return null; } + /** ?: branch throw `new` must not reuse merge phi / echo slot (#3802). */ + private function mergeEchoSlotForBranch(Block $branch): ?int + { + if (null === $branch->orig) { + return null; + } + foreach ($this->ternaryMergeTargets($branch->orig) as $mergeCfg) { + if ($this->seen->contains($mergeCfg)) { + $slot = $this->mergeEchoSlot($this->seen[$mergeCfg]); + if (null !== $slot) { + return $slot; + } + } + if ($this->ternaryMergeVarSlots->contains($mergeCfg)) { + /** @var SplObjectStorage $map */ + $map = $this->ternaryMergeVarSlots[$mergeCfg]; + foreach ($map as $root) { + return $map[$root]; + } + } + } + + return null; + } + protected function compileBlock(Block $block) { $this->compileOps($block->orig->children, $block); } @@ -726,6 +751,10 @@ protected function compileOps(array $ops, Block $block): void { break; } elseif ($this->isLoweredByFollowingCoalesce($child, $ops, $i)) { break; + } elseif ($this->isLoweredByFollowingThrow($child, $ops, $i)) { + break; + } elseif ($this->isUnreachableAfterThrow($child, $ops, $i)) { + break; } elseif ( $child instanceof Op\Expr\PropertyFetch && $i + 1 < $opCount @@ -2527,10 +2556,11 @@ protected function compileExpr(Op\Expr $expr, Block $block): array { $this->throwCompileError($msg); } } + $resultSlot = $this->compileOperand($expr->result, $block, false); $return = [ new OpCode( OpCode::TYPE_NEW, - $this->compileOperand($expr->result, $block, false), + $resultSlot, $this->compileOperand($expr->class, $block, true), ) ]; @@ -2574,12 +2604,7 @@ protected function compileExpr(Op\Expr $expr, Block $block): array { case Op\Expr\Isset_::class: return $this->compileIsset($expr, $block); case Op\Expr\Throw_::class: - $this->compileOrigExprForOperand($expr->expr, $block); - - return [new OpCode( - OpCode::TYPE_THROW, - $this->compileOperand($expr->expr, $block, true) - )]; + return $this->compileThrowExpression($expr, $block); case Op\Iterator\Valid::class: return [new OpCode( OpCode::TYPE_ITER_VALID, @@ -2836,12 +2861,16 @@ private function compileCoalesceRhsValue(Operand $rhs, Block $targetBlock, Block { $exprOp = $this->findOrigExprOpForOperand($rhs, $entryBlock); if (null !== $exprOp) { - foreach ($this->compileExpr($exprOp, $targetBlock) as $op) { - $targetBlock->addOpCode($op); - } if ($exprOp instanceof Op\Expr\Throw_) { + foreach ($this->compileThrowExpression($exprOp, $targetBlock, $entryBlock) as $op) { + $targetBlock->addOpCode($op); + } + return null; } + foreach ($this->compileExpr($exprOp, $targetBlock) as $op) { + $targetBlock->addOpCode($op); + } } return $this->compileOperand($rhs, $targetBlock, true); @@ -2884,6 +2913,149 @@ private function exprOpFeedsCoalesceRhs(Op\Expr $op, Op\Expr\BinaryOp\Coalesce $ return false; } + /** + * php-cfg emits inner expr ops (New_, …) before Throw_; lower them inside compileExpr(Throw_) (#3802). + * + * @param Op[] $ops + */ + private function isLoweredByFollowingThrow(Op $op, array $ops, int $index): bool + { + if (!$op instanceof Op\Expr) { + return false; + } + $count = count($ops); + for ($j = $index + 1; $j < $count; ++$j) { + $next = $ops[$j]; + if ($next instanceof Op\Expr\Throw_) { + return $this->exprOpFeedsThrowOperand($op, $next); + } + if (!$next instanceof Op\Expr) { + return false; + } + } + + return false; + } + + private function exprOpFeedsThrowOperand(Op\Expr $op, Op\Expr\Throw_ $throw): bool + { + return $this->operandsChainEqual($op->result, $throw->expr); + } + + /** + * Ops after throw-expr in the same CFG block are unreachable (?: arm, &&/|| RHS, = throw …) (#3802). + * + * @param Op[] $ops + */ + private function isUnreachableAfterThrow(Op $op, array $ops, int $index): bool + { + for ($j = $index - 1; $j >= 0; --$j) { + if ($ops[$j] instanceof Op\Expr\Throw_) { + return true; + } + if (!$ops[$j] instanceof Op\Expr) { + return false; + } + } + + return false; + } + + private function findThrowInnerExprOp(Op\Expr\Throw_ $throw, Block $block): ?Op\Expr + { + $root = $this->unwrapOperandChain($throw->expr); + if ($root instanceof Op\Expr) { + return $root; + } + + return $this->findOrigExprOpForOperand($throw->expr, $block); + } + + /** + * @return list + */ + private function compileThrowExpression(Op\Expr\Throw_ $expr, Block $block, Block ...$extraSearchBlocks): array + { + $newOp = $this->findNewExprForThrowOperand($expr, $block, ...$extraSearchBlocks); + $ops = []; + $throwSlot = null; + if (null !== $newOp) { + foreach ($this->compileNewExprForThrow($newOp, $block) as $innerOpcode) { + $ops[] = $innerOpcode; + } + $throwSlot = $this->compileOperand($newOp->result, $block, true); + } else { + $innerOp = $this->findThrowInnerExprOp($expr, $block); + if (null !== $innerOp) { + foreach ($this->compileExpr($innerOp, $block) as $innerOpcode) { + $ops[] = $innerOpcode; + } + } + } + if (null === $throwSlot) { + $throwSlot = $this->compileOperand($expr->expr, $block, true); + } + $ops[] = new OpCode( + OpCode::TYPE_THROW, + $throwSlot + ); + + return $ops; + } + + private function findNewExprForThrowOperand(Op\Expr\Throw_ $throw, Block ...$searchBlocks): ?Op\Expr\New_ + { + foreach ($searchBlocks as $searchBlock) { + if (null === $searchBlock->orig) { + continue; + } + foreach ($searchBlock->orig->children as $child) { + if ($child instanceof Op\Expr\New_ && $this->operandsChainEqual($child->result, $throw->expr)) { + return $child; + } + } + } + + return null; + } + + /** + * @return list + */ + private function compileNewExprForThrow(Op\Expr\New_ $expr, Block $block): array + { + $className = $this->literalScopeClassName($expr->class); + if (null !== $className) { + $lc = strtolower(ltrim($className, '\\')); + if (isset($this->abstractClasses[$lc])) { + $msg = isset($this->abstractEnums[$lc]) + ? 'Cannot instantiate enum '.$className + : 'Cannot instantiate abstract class '.$className; + $this->throwCompileError($msg); + } + } + $resultSlot = $block->forceFreshVarSlot($expr->result); + $mergeEcho = $this->mergeEchoSlotForBranch($block); + if (null !== $mergeEcho && $resultSlot === $mergeEcho) { + $resultSlot = $block->forceFreshVarSlot($expr->result); + } + $return = [ + new OpCode( + OpCode::TYPE_NEW, + $resultSlot, + $this->compileOperand($expr->class, $block, true), + ), + ]; + foreach ($this->compileCallArgSends($expr->args, $block) as $send) { + $return[] = $send; + } + $return[] = new OpCode( + OpCode::TYPE_FUNCCALL_EXEC_NORETURN + ); + + return $return; + } + private function compileOrigExprForOperand(Operand $operand, Block $block): void { $exprOp = $this->findOrigExprOpForOperand($operand, $block); diff --git a/script/capability-syntax-lib.php b/script/capability-syntax-lib.php index 8bb8f7d2022..5fce755624b 100644 --- a/script/capability-syntax-lib.php +++ b/script/capability-syntax-lib.php @@ -550,6 +550,18 @@ function syntaxRowDefinitions(): array ], 'probe' => 'class E {} try { throw new E(); } catch (E $e) { echo "ok"; }', ], + [ + 'id' => 'throw_expression', + 'construct' => 'throw expressions (PHP 8.0) — `throw` in expression context', + 'opcodes' => ['TYPE_THROW', 'TYPE_NEW', 'TYPE_JUMPIF'], + 'issue' => 3802, + 'notes' => [ + 'php-cfg Op\\Expr\\Throw_ overlay (#3802); php-types Expr_Throw type reconstructor patch', + 'Compiler: skip duplicate New before Throw; fresh slot for ?: merge; ?? RHS via compileThrowExpression', + 'VM/JIT reuse TYPE_THROW; compliance throw_expression.phpt (?:, ??, &&)', + ], + 'probe' => 'try { echo (false ? 1 : throw new LogicException("x")); } catch (LogicException $e) { echo $e->getMessage(); }', + ], [ 'id' => 'readonly_class', 'construct' => 'readonly classes', @@ -760,6 +772,7 @@ function collectSyntaxPhptCoverage(string $root, array $definitions): array 'array_argument_unpack' => '/\.\.\.\s*\$/', 'multi_catch' => '/catch\s*\([^)]*\|/', 'try_catch_throw' => '/\btry\s*\{/', + 'throw_expression' => '/\?\s*:\s*throw\b|\?\?\s*throw\b|&&\s*throw\b|\|\|\s*throw\b/', 'heredoc_flexible_indent' => '/<<<\s*\w+\s*\r?\n\s+\S/', ]; diff --git a/test/compliance/cases/language/throw_expression.phpt b/test/compliance/cases/language/throw_expression.phpt new file mode 100644 index 00000000000..e5346f91425 --- /dev/null +++ b/test/compliance/cases/language/throw_expression.phpt @@ -0,0 +1,40 @@ +--TEST-- +Language: throw expressions — ternary, ??, && (PHP 8.0, Zend zend_compile.c #3802) +--FILE-- +getMessage(), "\n"; +} + +// null coalesce RHS +$missing = null; +try { + echo ($missing ?? throw new LogicException('coalesce')), "\n"; +} catch (LogicException $e) { + echo 'caught:', $e->getMessage(), "\n"; +} + +// short-circuit && RHS when LHS is true +try { + echo (true && throw new LogicException('and')), "\n"; +} catch (LogicException $e) { + echo 'caught:', $e->getMessage(), "\n"; +} + +// short-circuit && skips throw when LHS is false +$hit = 0; +try { + echo (false && throw new LogicException('skip')), "\n"; +} catch (LogicException $e) { + $hit = 1; +} +echo $hit, "\n"; +?> +--EXPECT-- +caught:ternary +caught:coalesce +caught:and +0