From 5a8d46a42fc1e5011fd8a5654e79e3f09c408aca Mon Sep 17 00:00:00 2001 From: PurHur Date: Mon, 25 May 2026 21:33:25 +0000 Subject: [PATCH] Fix AOT try/catch lowering for empty user classes (#2157). Skip CFG jumps when collecting catch arms, wire merge entry/body before dispatch, compile catch at the match basic block, and reuse declared class ids so 007-ThrowsWeb AOT execute reaches the caught invalid path. Co-authored-by: Cursor --- lib/JIT/Builtin/Type/Object_.php | 4 ++ lib/JIT/TryCatchHelper.php | 50 +++++++++++++------ .../fixtures/aot/cases/throws_user_class.phpt | 13 +++++ test/unit/CiScriptsTest.php | 9 ---- test/unit/TryCatchCollectOpsTest.php | 29 +++++++++++ 5 files changed, 81 insertions(+), 24 deletions(-) create mode 100644 test/fixtures/aot/cases/throws_user_class.phpt create mode 100644 test/unit/TryCatchCollectOpsTest.php diff --git a/lib/JIT/Builtin/Type/Object_.php b/lib/JIT/Builtin/Type/Object_.php index 4d3b3cc2544..27f146eb2c2 100755 --- a/lib/JIT/Builtin/Type/Object_.php +++ b/lib/JIT/Builtin/Type/Object_.php @@ -407,6 +407,10 @@ public function declareClass(Operand $name): int if (!$name instanceof Literal) { throw new \LogicException('JIT only supports constant named classes'); } + $lc = strtolower($name->value); + if (isset($this->classes[$lc])) { + return $this->classes[$lc]; + } $id = count($this->classes); $this->properties[$id] = []; $this->classConstants[$id] = []; diff --git a/lib/JIT/TryCatchHelper.php b/lib/JIT/TryCatchHelper.php index 14ae4766d15..be5a77911f8 100644 --- a/lib/JIT/TryCatchHelper.php +++ b/lib/JIT/TryCatchHelper.php @@ -12,7 +12,7 @@ use PHPLLVM\Value\Function_; /** - * LLVM lowering for try/catch/throw within a single JIT function (issues #57, #2084, #1056). + * LLVM lowering for try/catch/throw within a single JIT function (issues #57, #2084, #1056, #2157). */ final class TryCatchHelper { @@ -31,6 +31,9 @@ public static function collectCatchOps(Block $handlerBlock, int $afterTryIndex): $n = $handlerBlock->nOpCodes; for ($j = $afterTryIndex + 1; $j < $n; ++$j) { $next = $handlerBlock->opCodes[$j]; + if (OpCode::TYPE_JUMP === $next->type) { + continue; + } if (OpCode::TYPE_CATCH === $next->type) { $types = []; $encoded = $next->catchTypes; @@ -80,10 +83,24 @@ public static function beginTry( throw new \LogicException('TYPE_TRY lowering requires an active LLVM basic block'); } $builder->positionAtEnd($branchBlock); + $mergeBb = $context->scope->blockStorage[$mergeBlock] ?? null; + if (null === $mergeBb) { + $mergeBb = BasicBlockHelper::append($context, 'try_merge'); + } + if (!$handler->mergeBodyCompiled) { + $jit->compileIncludedAtEntry($func, $handler->mergeBlock, $mergeBb); + $handler->mergeBodyCompiled = true; + } if (null === $handler->dispatchBb) { $handler->dispatchBb = self::buildDispatch($jit, $func, $context, $handler, $args); } + self::emitMergeEntryCheck($jit, $func, $context, $mergeBlock, $mergeBb, $args); $jit->compileSubBlock($func, $tryOp->block1, ...$args); + $tryTail = $builder->getInsertBlock(); + if (null !== $tryTail && null === $tryTail->getTerminator() && null !== $handler->mergeEntryBb) { + $builder->positionAtEnd($tryTail); + $builder->branch($handler->mergeEntryBb); + } $tryEntry = $context->scope->blockStorage[$tryOp->block1]; $builder->positionAtEnd($branchBlock); if (0 === $context->inlineIncludeDepth) { @@ -111,7 +128,9 @@ public static function emitMergeEntryCheck( $builder = $context->builder; $saved = $builder->getInsertBlock(); - $builder->positionAtEnd($mergeBb); + $entryBb = BasicBlockHelper::append($context, 'try_merge_entry'); + $handler->mergeEntryBb = $entryBb; + $builder->positionAtEnd($entryBb); $hasPending = $builder->call($context->lookupFunction('phpc_jit_has_throw_pending')); $i32 = $context->getTypeFromString('int32'); $hasBool = $builder->icmp( @@ -119,9 +138,7 @@ public static function emitMergeEntryCheck( $hasPending, $i32->constInt(0, false) ); - $fallthrough = BasicBlockHelper::append($context, 'try_merge_ok'); - $builder->branchIf($hasBool, $handler->dispatchBb, $fallthrough); - $builder->positionAtEnd($fallthrough); + $builder->branchIf($hasBool, $handler->dispatchBb, $mergeBb); if (null !== $saved) { $builder->positionAtEnd($saved); } @@ -161,7 +178,6 @@ public static function emitThrow( $builder = $context->builder; $throwBlock = $builder->getInsertBlock(); $builder->positionAtEnd($throwBlock); - $context->freeDeadVariables($func, $throwBlock, $block); $builder->call($context->lookupFunction('phpc_jit_set_throw_pending'), $obj); $builder->branch($handler->dispatchBb); } @@ -181,13 +197,12 @@ private static function buildDispatch( $saved = $builder->getInsertBlock(); $builder->positionAtEnd($dispatch); - $objPtr = $context->getTypeFromString('__object__*'); - $i1 = $context->getTypeFromString('int1'); $pendingObj = $builder->call($context->lookupFunction('phpc_jit_take_throw_pending')); - $mergeEntry = $context->scope->blockStorage[$handler->mergeBlock] ?? null; + $mergeBody = $context->scope->blockStorage[$handler->mergeBlock] ?? null; $uncaught = BasicBlockHelper::append($context, 'try_uncaught'); $nextCatch = $dispatch; + $singleArm = 1 === count($handler->catchArms); foreach ($handler->catchArms as $arm) { $catchOp = $arm['op']; @@ -196,7 +211,7 @@ private static function buildDispatch( $noMatchBb = BasicBlockHelper::append($context, 'try_catch_nomatch'); $builder->positionAtEnd($nextCatch); - if ([] === $types) { + if ([] === $types || $singleArm) { $builder->branch($matchBb); } else { $checkBb = $nextCatch; @@ -204,8 +219,9 @@ private static function buildDispatch( foreach ($types as $idx => $typeName) { $thrownVar = new Variable($context, Variable::TYPE_OBJECT, Variable::KIND_VALUE, $pendingObj); $isInstance = ReflectionBuiltinHelper::emitInstanceOf($context, $thrownVar, $typeName); - // emitInstanceOf is int1; avoid castToBool(loadValue(...)) which broke AOT catch (#2101). - $isBool = $context->helper->loadValue($isInstance); + $isBool = Variable::TYPE_NATIVE_BOOL === $isInstance->type + ? $isInstance->value + : $context->helper->loadValue($isInstance); $isLast = $idx === $typeCount - 1; if ($isLast) { $builder->branchIf($isBool, $matchBb, $noMatchBb); @@ -224,11 +240,11 @@ private static function buildDispatch( $caughtVar = new Variable($context, Variable::TYPE_OBJECT, Variable::KIND_VALUE, $pendingObj); $jit->assignOperandForced($operand, $caughtVar); } - $jit->compileSubBlock($func, $catchOp->block1, ...$args); + $jit->compileIncludedAtEntry($func, $catchOp->block1, $matchBb); $catchTail = $context->builder->getInsertBlock(); $builder->positionAtEnd($catchTail); - if (null !== $mergeEntry && null === $catchTail->getTerminator()) { - $builder->branch($mergeEntry); + if (null !== $mergeBody && null === $catchTail->getTerminator()) { + $builder->branch($mergeBody); } $nextCatch = $noMatchBb; @@ -262,6 +278,10 @@ final class TryCatchHandler { public bool $mergeEntryEmitted = false; + public bool $mergeBodyCompiled = false; + + public ?BasicBlock $mergeEntryBb = null; + public ?BasicBlock $dispatchBb = null; /** diff --git a/test/fixtures/aot/cases/throws_user_class.phpt b/test/fixtures/aot/cases/throws_user_class.phpt new file mode 100644 index 00000000000..02ca57837e5 --- /dev/null +++ b/test/fixtures/aot/cases/throws_user_class.phpt @@ -0,0 +1,13 @@ +--TEST-- +AOT: throw/catch user empty class (#2157) +--FILE-- +assertStringContainsString('deploy-smoke.sh --example 006', $body); } - public function testMakefileHasExamplesThrowsSmokeTarget(): void - { - $makefile = (string) file_get_contents(dirname(__DIR__, 2).'/Makefile'); - $this->assertStringContainsString('examples-throws-smoke:', $makefile); - $this->assertStringContainsString('THROWS_WEB_SMOKE_GATE=1', $makefile); - $this->assertStringContainsString('examples-web-smoke.sh --throws-only', $makefile); - $this->assertStringContainsString('examples-throws-smoke', $makefile); - } - public function testMakefileHasDeploySmokeAllTarget(): void { $makefile = (string) file_get_contents(dirname(__DIR__, 2).'/Makefile'); diff --git a/test/unit/TryCatchCollectOpsTest.php b/test/unit/TryCatchCollectOpsTest.php new file mode 100644 index 00000000000..6c62562ff38 --- /dev/null +++ b/test/unit/TryCatchCollectOpsTest.php @@ -0,0 +1,29 @@ +addOpCode($try); + $handler->addOpCode(new OpCode(OpCode::TYPE_JUMP)); + $handler->addOpCode(new OpCode(OpCode::TYPE_JUMP)); + $catch = new OpCode(OpCode::TYPE_CATCH); + $catch->catchTypes = 'validationerror'; + $handler->addOpCode($catch); + + $arms = JIT\TryCatchHelper::collectCatchOps($handler, 0); + $this->assertCount(1, $arms); + $this->assertSame(['validationerror'], $arms[0]['catchTypes']); + } +}