Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions lib/Block.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
192 changes: 182 additions & 10 deletions lib/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<CfgVariable, int> $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);
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
)
];
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<OpCode>
*/
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<OpCode>
*/
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);
Expand Down
13 changes: 13 additions & 0 deletions script/capability-syntax-lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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/',
];

Expand Down
40 changes: 40 additions & 0 deletions test/compliance/cases/language/throw_expression.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
--TEST--
Language: throw expressions — ternary, ??, && (PHP 8.0, Zend zend_compile.c #3802)
--FILE--
<?php
// ternary false arm
try {
echo (false ? 1 : throw new LogicException('ternary')), "\n";
} catch (LogicException $e) {
echo 'caught:', $e->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