diff --git a/lib/Block.php b/lib/Block.php index 3aef86c4f1f..c14ddd046b8 100755 --- a/lib/Block.php +++ b/lib/Block.php @@ -88,6 +88,9 @@ class Block { /** Function body contains `yield` (issue #167). */ public bool $isGenerator = false; + /** Closure `use ($var)` slots populated at call from {@see ClosureState} (issue #72). */ + public array $closureCaptureSlots = []; + /** Resolved absolute paths for TYPE_INCLUDE opcodes (arg3 index, issue #54). */ public array $literalIncludePaths = []; @@ -240,7 +243,7 @@ public function inheritScopeFrom(Block $parent): void if ($parent->args->contains($operand)) { $this->args[$operand] = $slot; } - if (isset($parent->constants[$slot])) { + if (isset($parent->constants[$slot]) && !isset($this->constants[$slot])) { $this->constants[$slot] = $parent->constants[$slot]; } } @@ -338,7 +341,7 @@ private static function findVariableInParentFrames(Operand $op, Frame $frame): ? return self::findVariableInParentFramesByName($name, $frame); } - private static function findVariableInParentFramesByName(string $name, Frame $frame): ?Variable + public static function findVariableInParentFramesByName(string $name, Frame $frame): ?Variable { for ($f = $frame; null !== $f; $f = $f->parent) { if ('this' === $name && !empty($f->calledArgs)) { @@ -388,6 +391,8 @@ public function getFrame(Context $context, ?Frame $frame = null): Frame { if (isset($this->constants[$pos])) { $scope[$pos] = $this->constants[$pos]; + } elseif (isset($this->closureCaptureSlots[$pos])) { + $scope[$pos] = self::initialVariableForOperand($op, $context, $pos, $this); } elseif ($this->args->contains($op)) { if (is_null($frame)) { $scope[$pos] = self::initialVariableForOperand($op, $context, $pos, $this); diff --git a/lib/Compiler.php b/lib/Compiler.php index ad125b351db..cf1122397cb 100755 --- a/lib/Compiler.php +++ b/lib/Compiler.php @@ -1510,6 +1510,7 @@ protected function compileStmt(Op\Stmt $stmt, Block $block) { $finallyOp->block1 = $compiledFinally; $finallyOp->block2 = $merge; $block->addOpCode($finallyOp); + $this->rewriteTryMergeJumpsToFinally($try, $merge, $compiledFinally); } } elseif ($stmt instanceof Op\Stmt\Switch_) { $this->compileSwitchAsJumpIfChain($stmt, $block); @@ -2021,9 +2022,6 @@ protected function compileAnonymousFunctionExpr($expr, Block $block): array $nullSlot )]; } - if ($expr instanceof Op\Expr\Closure && [] !== $expr->useVars) { - $this->throwCompileLogic('Closure use() captures are not supported yet (issue #72)'); - } $func = $expr->func; $funcBlock = $this->compileCfgBlock($func->cfg, $func->params, $func); $op = new OpCode( @@ -2031,10 +2029,33 @@ protected function compileAnonymousFunctionExpr($expr, Block $block): array $this->compileOperand($expr->result, $block, false), ); $op->block1 = $funcBlock; + if ($expr instanceof Op\Expr\Closure) { + foreach ($expr->useVars as $useVar) { + if (!$useVar instanceof Operand\BoundVariable) { + continue; + } + $name = $this->boundVariableName($useVar); + $slot = $funcBlock->getVarSlot($useVar, false); + $funcBlock->closureCaptureSlots[$slot] = true; + $op->closureCaptures[] = [ + 'name' => $name, + 'slot' => $slot, + 'byRef' => $useVar->byRef, + ]; + } + } return [$op]; } + private function boundVariableName(Operand\BoundVariable $useVar): string + { + if ($useVar->name instanceof Operand\Literal && is_string($useVar->name->value)) { + return $useVar->name->value; + } + $this->throwCompileLogic('Closure use() variable name must be a literal'); + } + protected function shouldStubClosureForBootstrap(): bool { return '1' === (string) getenv('PHP_COMPILER_VENDOR_PRELINK') @@ -2763,8 +2784,18 @@ protected function compileBoolConstant(Block $block, bool $value): int } /** - * @param list $types + * Normal try completion must run finally before merge; php-cfg jumps try straight to end (#2114). */ + private function rewriteTryMergeJumpsToFinally(Block $try, Block $merge, Block $finally): void + { + for ($i = 0; $i < $try->nOpCodes; ++$i) { + $op = $try->opCodes[$i]; + if (OpCode::TYPE_JUMP === $op->type && $op->block1 === $merge) { + $op->block1 = $finally; + } + } + } + /** * php-cfg TryCatch emits a Stmt_Jump into the try body; TYPE_TRY already enters it (#2084). */ diff --git a/lib/Frame.php b/lib/Frame.php index c9d93d7afdc..bd9ce6860fd 100755 --- a/lib/Frame.php +++ b/lib/Frame.php @@ -47,6 +47,9 @@ class Frame { /** Active generator while executing a generator function body (issue #167). */ public ?VM\GeneratorState $generatorState = null; + /** Pending closure call: captures bound when the callee frame is entered (issue #72). */ + public ?VM\ClosureState $closureCall = null; + /** Set when TYPE_YIELD suspends; runFrames returns GENERATOR_YIELD. */ public bool $generatorYield = false; diff --git a/lib/JIT.php b/lib/JIT.php index 84d28a421e5..17ba0a6d7b2 100644 --- a/lib/JIT.php +++ b/lib/JIT.php @@ -5358,15 +5358,29 @@ private function compileBlockInternal( $this->compileBlock($op->block1, $nameOp->value); break; case OpCode::TYPE_CLOSURE: - // Bootstrap stub: closures are not executable yet; represent as null. - $nullVar = new Variable( + if ($this->shouldUseSelfHostJitStubs() || null === $op->block1) { + // Bootstrap / vendor prelink: closures are not executable yet; represent as null. + $nullVar = new Variable( + $this->context, + Variable::TYPE_NULL, + Variable::KIND_VALUE, + $this->context->getTypeFromString('__value__*')->constNull() + ); + $nullVar->isNullConstant = true; + $this->assignOperandValue($block->getOperand($op->arg1), $nullVar->value); + break; + } + $internalName = JIT\ClosureHelper::nextInternalName(); + $this->compileBlock($op->block1, $internalName); + $lcname = strtolower($internalName); + if (!isset($this->context->functionProxies[$lcname])) { + throw new \LogicException("Closure body failed to register JIT proxy: {$internalName}"); + } + $closureObj = JIT\ClosureHelper::allocateClosureObject( $this->context, - Variable::TYPE_NULL, - Variable::KIND_VALUE, - $this->context->getTypeFromString('__value__*')->constNull() + $this->context->functionProxies[$lcname] ); - $nullVar->isNullConstant = true; - $this->assignOperandValue($block->getOperand($op->arg1), $nullVar->value); + $this->assignOperand($block->getOperand($op->arg1), $closureObj, true); break; case OpCode::TYPE_YIELD: case OpCode::TYPE_YIELD_FROM: @@ -5379,6 +5393,13 @@ private function compileBlockInternal( $this->context->scope->toCall = $this->context->resolveFunctionProxy($lcname); } else { if (null !== $nameOp->type && Type::TYPE_OBJECT === $nameOp->type->type) { + $calleeVar = $this->context->getVariableFromOp($nameOp); + $closureCall = JIT\ClosureHelper::resolveCall($calleeVar); + if (null !== $closureCall) { + $this->context->scope->toCall = $closureCall; + $this->context->scope->args = []; + break; + } $this->initJitMethodCall($block, $nameOp, '__invoke'); break; } @@ -7142,6 +7163,7 @@ private function copyObjectPropertyBacking(Variable $dest, Variable $src): void $dest->objectPropertyReceiver = $src->objectPropertyReceiver; $dest->objectPropertyName = $src->objectPropertyName; $dest->objectPropertyClassName = $src->objectPropertyClassName; + $dest->closureCall = $src->closureCall; } private function markJitThisConstructedIfLeavingConstruct(Block $block): void @@ -7425,6 +7447,16 @@ private function instanceMethodUsesThis(Block $block): bool */ private function initJitMethodCall(Block $block, Operand $receiverOp, string $methodName): void { + if ('__invoke' === strtolower($methodName)) { + $receiver = $this->context->getVariableFromOp($receiverOp); + $closureCall = JIT\ClosureHelper::resolveCall($receiver); + if (null !== $closureCall) { + $this->context->scope->toCall = $closureCall; + $this->context->scope->args = []; + + return; + } + } if (null === $receiverOp->type) { // Bootstrap/self-host can hit methodcall init before operand typing stabilizes. // Prefer a safe short-circuit for stubbed self-host JIT paths over hard-crashing. diff --git a/lib/JIT/Builtin/Type/Object_.php b/lib/JIT/Builtin/Type/Object_.php index 74b2cb8dcea..0e42df6707b 100755 --- a/lib/JIT/Builtin/Type/Object_.php +++ b/lib/JIT/Builtin/Type/Object_.php @@ -885,6 +885,9 @@ private function registerExternalClass(string $lcname, string $displayName): voi $this->defineProperty($id, 'mode', Variable::TYPE_NATIVE_LONG); } } + if ('closure' === $lcname) { + // Marker class for JIT-lowered closures; invoke target lives on Variable::$closureCall (#72). + } if ('splobjectstorage' === $lcname) { $this->splObjectStorageClassId = $id; $this->defineProperty($id, '__spl_ht', Variable::TYPE_HASHTABLE); diff --git a/lib/JIT/ClosureHelper.php b/lib/JIT/ClosureHelper.php new file mode 100644 index 00000000000..a5a048c649e --- /dev/null +++ b/lib/JIT/ClosureHelper.php @@ -0,0 +1,37 @@ +closureCall; + } + + public static function allocateClosureObject(Context $context, Call $callProxy): Variable + { + $classId = $context->type->object->lookup('Closure'); + $obj = $context->type->object->allocate($classId); + $context->type->object->markObjectConstructed($obj); + $var = new Variable($context, Variable::TYPE_OBJECT, Variable::KIND_VALUE, $obj); + $var->closureCall = $callProxy; + + return $var; + } +} diff --git a/lib/JIT/Variable.php b/lib/JIT/Variable.php index 0da26e3a878..1b79d6eaf4e 100755 --- a/lib/JIT/Variable.php +++ b/lib/JIT/Variable.php @@ -119,6 +119,9 @@ final class Variable { /** Declaring class name for readonly diagnostics (#1360). */ public ?string $objectPropertyClassName = null; + /** Native call proxy when this object is a JIT-lowered closure (#72). */ + public ?Call $closureCall = null; + private static int $lvalueCounter = 0; public int $nextFreeElement = 0; diff --git a/lib/OpCode.php b/lib/OpCode.php index f102d13c55a..9af18d7253e 100755 --- a/lib/OpCode.php +++ b/lib/OpCode.php @@ -164,6 +164,13 @@ class OpCode { /** Pipe-separated lowercase catch class names for TYPE_CATCH (#1362). */ public ?string $catchTypes = null; + /** + * Closure `use ($var)` metadata for TYPE_CLOSURE (issue #72). + * + * @var list + */ + public array $closureCaptures = []; + public function __construct(int $type, ?int $arg1 = null, ?int $arg2 = null, ?int $arg3 = null) { $this->type = $type; $this->arg1 = $arg1; diff --git a/lib/VM.php b/lib/VM.php index 80fa970e092..9b22ff3507d 100755 --- a/lib/VM.php +++ b/lib/VM.php @@ -340,6 +340,11 @@ private function runFrames(): int ext\standard\VmExit::terminate($exitArg); break; case OpCode::TYPE_JUMP: + $resumeFrame = $this->resumeCatchAfterFinally($frame); + if (null !== $resumeFrame) { + $frame = $resumeFrame; + goto restart; + } $frame = $this->frameForBranch($frame, $op->block1); goto restart; case OpCode::TYPE_JUMPIF: @@ -517,7 +522,8 @@ private function runFrames(): int ? $op->block1->func->name : '{closure}'; $closureFunc = new Func\PHP($funcName, $op->block1); - $state = new ClosureState($closureFunc); + $captures = $this->bindClosureCaptures($frame, $op->closureCaptures); + $state = new ClosureState($closureFunc, $captures); $frame->scope[$op->arg1]->object($state->wrapObject($this->context)); break; case OpCode::TYPE_RETURN_VOID: @@ -579,6 +585,7 @@ private function runFrames(): int $closureState = $callee->toObject()->closureState; if (null !== $closureState) { $frame->call = $closureState->func; + $frame->closureCall = $closureState; $frame->callArgs = []; $frame->callArgEntries = []; break; @@ -659,6 +666,8 @@ private function runFrames(): int break; } $new = $frame->call->getFrame($this->context, $frame); + $this->bindClosureCallCaptures($new, $frame->closureCall); + $frame->closureCall = null; $new->calledClass = $this->inferCalledClass($frame); $new->returnVar = null; if ($op->type === OpCode::TYPE_FUNCCALL_EXEC_RETURN) { @@ -1099,8 +1108,8 @@ private function runFrames(): int if (null !== $this->context->pendingException) { break; } - if (null !== $op->block2) { - $frame = $op->block2->getFrame($this->context, $frame); + if (null !== $op->block1) { + $frame = $op->block1->getFrame($this->context, $frame); goto restart; } break; @@ -1111,16 +1120,8 @@ private function runFrames(): int $frame = $catchFrame; goto restart; } - if (Variable::TYPE_OBJECT === $thrown->type) { - $entry = $thrown->toObject(); - try { - $message = $entry->getProperty('message')->toString(); - } catch (\LogicException) { - $message = 'Exception'; - } - throw new \Exception($message); - } - throw new \Exception($thrown->toString()); + $this->raiseUncaughtException($thrown); + break; default: throw new \LogicException("VM OpCode Not Implemented: " . opcode_type_name($op->type)); } @@ -1172,12 +1173,16 @@ private function findCatchFrameForThrow(Frame $frame, Variable $thrown): ?Frame $this->context->pendingException = $thrown; for ($handler = $frame->parent ?? $frame; null !== $handler; $handler = $handler->parent) { $this->rewindHandlerToCatchChain($handler); + $finallyFrame = $this->enterFinallyHandlerForThrow($handler); + if (null !== $finallyFrame) { + return $finallyFrame; + } $catchFrame = $this->enterMatchingCatchHandler($handler); if (null !== $catchFrame) { return $catchFrame; } } - $this->context->pendingException = null; + $this->clearTryCatchUnwindState(); return null; } @@ -1240,6 +1245,7 @@ private function enterMatchingCatchHandler(Frame $handler): ?Frame $handler->pos = $handler->block->nOpCodes; $catchFrame->parent = $mergeFrame; } + $this->clearTryCatchUnwindState(); return $catchFrame; } @@ -1247,6 +1253,79 @@ private function enterMatchingCatchHandler(Frame $handler): ?Frame return null; } + private function enterFinallyHandlerForThrow(Frame $handler): ?Frame + { + $handlerId = spl_object_id($handler); + if (isset($this->context->completedFinallyHandlers[$handlerId])) { + return null; + } + $finallyOp = $this->findFinallyOpForHandler($handler); + if (null === $finallyOp || null === $finallyOp->block1) { + return null; + } + $this->context->completedFinallyHandlers[$handlerId] = true; + $this->context->pendingCatchResumeHandler = $handler; + + return $finallyOp->block1->getFrame($this->context, $handler); + } + + private function findFinallyOpForHandler(Frame $handler): ?OpCode + { + foreach ($handler->block->opCodes as $op) { + if (OpCode::TYPE_FINALLY === $op->type) { + return $op; + } + } + + return null; + } + + private function resumeCatchAfterFinally(Frame $frame): ?Frame + { + $handler = $this->context->pendingCatchResumeHandler; + if (null === $handler) { + return null; + } + $this->context->pendingCatchResumeHandler = null; + $this->rewindHandlerToCatchChain($handler); + $catchFrame = $this->enterMatchingCatchHandler($handler); + if (null !== $catchFrame) { + return $catchFrame; + } + $thrown = $this->context->pendingException; + if (null === $thrown) { + return null; + } + $outerCatch = $this->findCatchFrameForThrow($handler->parent ?? $handler, $thrown); + if (null !== $outerCatch) { + return $outerCatch; + } + $this->raiseUncaughtException($thrown); + } + + private function clearTryCatchUnwindState(): void + { + $this->context->pendingException = null; + $this->context->pendingCatchResumeHandler = null; + $this->context->completedFinallyHandlers = []; + } + + /** @return never */ + private function raiseUncaughtException(Variable $thrown): void + { + $this->clearTryCatchUnwindState(); + if (Variable::TYPE_OBJECT === $thrown->type) { + $entry = $thrown->toObject(); + try { + $message = $entry->getProperty('message')->toString(); + } catch (\LogicException) { + $message = 'Exception'; + } + throw new \Exception($message); + } + throw new \Exception($thrown->toString()); + } + /** * After a catch match, skip remaining TYPE_CATCH / CFG entry TYPE_JUMP on the handler * block so merge fallthrough does not re-enter the try body (#2084). @@ -1453,6 +1532,49 @@ protected function scopeSlot(Frame $frame, int $slot): Variable return $frame->scope[$slot]; } + /** + * @param list $captureSpecs + * + * @return list + */ + protected function bindClosureCaptures(Frame $frame, array $captureSpecs): array + { + $captures = []; + foreach ($captureSpecs as $spec) { + $src = Block::findVariableInParentFramesByName($spec['name'], $frame); + $stored = new Variable(); + if (null === $src) { + $stored->null(); + } elseif ($spec['byRef']) { + $stored->indirect($src->resolveIndirect()); + } else { + $stored->copyFrom($src->resolveIndirect()); + } + $captures[] = [ + 'slot' => $spec['slot'], + 'var' => $stored, + 'byRef' => $spec['byRef'], + ]; + } + + return $captures; + } + + protected function bindClosureCallCaptures(Frame $callee, ?ClosureState $closureState): void + { + if (null === $closureState || [] === $closureState->captures) { + return; + } + foreach ($closureState->captures as $capture) { + $dest = $this->scopeSlot($callee, $capture['slot']); + if ($capture['byRef']) { + $dest->indirect($capture['var']->resolveIndirect()); + } else { + $dest->copyFrom($capture['var']); + } + } + } + protected function resolveStaticClassName(string $className, Frame $frame): string { return $this->resolveClassScopeName($className, $frame); @@ -1525,6 +1647,7 @@ protected function initMethodCall(Frame $frame, Variable $receiver, string $meth $object = $receiver->toObject(); if (null !== $object->closureState && '__invoke' === $methodLc) { $frame->call = $object->closureState->func; + $frame->closureCall = $object->closureState; $frame->callArgs = []; $frame->callArgEntries = []; diff --git a/lib/VM/ClosureState.php b/lib/VM/ClosureState.php index 1c5a6a56a4f..c5e5e7dcde6 100644 --- a/lib/VM/ClosureState.php +++ b/lib/VM/ClosureState.php @@ -14,9 +14,21 @@ */ final class ClosureState { + /** + * Bound `use ($var)` values captured when the closure object was created. + * + * @var list + */ + public array $captures; + + /** + * @param list $captures + */ public function __construct( public readonly Func\PHP $func, + array $captures = [], ) { + $this->captures = $captures; } public static function register(Context $ctx): void diff --git a/lib/VM/Context.php b/lib/VM/Context.php index e92f37b6b4e..31db33163c3 100755 --- a/lib/VM/Context.php +++ b/lib/VM/Context.php @@ -45,6 +45,12 @@ class Context { /** Pending thrown value while dispatching catch handlers (issue #1362). */ public ?Variable $pendingException = null; + /** Handler frame whose catch chain resumes after a throw-path finally (issue #2114). */ + public ?Frame $pendingCatchResumeHandler = null; + + /** @var array handler frame object id => finally already ran for current unwind */ + public array $completedFinallyHandlers = []; + public ErrorReporter $errors; public ScriptStack $scriptStack; diff --git a/script/capability-syntax-lib.php b/script/capability-syntax-lib.php index e555287d230..3c78194b9f2 100644 --- a/script/capability-syntax-lib.php +++ b/script/capability-syntax-lib.php @@ -157,7 +157,7 @@ function syntaxRowDefinitions(): array 'issue' => 72, 'jit' => false, 'aot' => false, - 'notes' => ['VM via ClosureState + __invoke; bootstrap stubs null; use() deferred'], + 'notes' => ['VM via ClosureState + __invoke; bootstrap stubs null; use() by-value on VM; use (&$x) deferred'], 'probe' => '$f = function ($x) { return $x + 1; }; echo $f(2);', ], [ diff --git a/test/compliance/ClosureVMTest.php b/test/compliance/ClosureVMTest.php index 9ad13b1d6e2..51138c660e2 100644 --- a/test/compliance/ClosureVMTest.php +++ b/test/compliance/ClosureVMTest.php @@ -16,7 +16,7 @@ public function setUp(): void public static function providePHPTests(): \Generator { - foreach (['closure_simple.phpt', 'closure_arrow.phpt', 'closure_in_array.phpt'] as $file) { + foreach (['closure_simple.phpt', 'closure_arrow.phpt', 'closure_in_array.phpt', 'closure_use.phpt'] as $file) { $path = __DIR__ . '/cases/language/' . $file; $name = preg_replace('/\.phpt$/', '', $file) ?: $file; yield $name => self::parsePHPT($path, $file); diff --git a/test/compliance/cases/language/closure_use.phpt b/test/compliance/cases/language/closure_use.phpt new file mode 100644 index 00000000000..ab770d9b63f --- /dev/null +++ b/test/compliance/cases/language/closure_use.phpt @@ -0,0 +1,14 @@ +--TEST-- +language: closure use() captures by value (issue #72) +--FILE-- +repoRoot = dirname(__DIR__, 2); + if (!LlvmToolchain::isReady($this->repoRoot)) { + $reason = LlvmToolchain::readyFailureReason() ?? 'LLVM 9 toolchain not available'; + $this->markTestSkipped($reason.' — closure JIT compile test needs LLVM (#72)'); + } + } + + public function testClosureSimpleModuleVerifies(): void + { + $code = $this->fixtureCode('closure_simple.phpt'); + $runtime = new Runtime(); + $block = $runtime->parseAndCompile($code, 'closure_simple.phpt'); + $runtime->jitCompileBlock($block); + + $context = $runtime->loadJitContext(); + $verify = new \ReflectionMethod($context, 'compileCommon'); + $verify->setAccessible(true); + $verify->invoke($context); + } + + public function testBinJitRunClosureInline(): void + { + if (!$this->jitProbeOk()) { + $this->markTestSkipped('JIT MCJIT probe failed — bin/jit.php not runnable (#72)'); + } + $jit = realpath($this->repoRoot.'/bin/jit.php'); + if (false === $jit) { + $this->markTestSkipped('bin/jit.php missing'); + } + $code = '$f = function($x) { return $x + 1; }; echo $f(2);'; + $env = $this->llvmProcessEnv(); + $cmd = array_merge( + LlvmToolchain::envPrefix($this->repoRoot), + [PHP_BINARY, $jit, '-r', $code] + ); + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $proc = proc_open($cmd, $descriptorSpec, $pipes, $this->repoRoot, $env); + $this->assertIsResource($proc); + fclose($pipes[0]); + $stdout = stream_get_contents($pipes[1]); + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + $exit = proc_close($proc); + $combined = trim(($stdout !== false ? $stdout : '').($stderr !== false ? $stderr : '')); + $this->assertSame(0, $exit, $combined); + $this->assertStringContainsString('3', $combined); + } + + private function fixtureCode(string $file): string + { + $path = $this->repoRoot.'/test/compliance/cases/language/'.$file; + $contents = file_get_contents($path); + $this->assertNotFalse($contents); + if (!preg_match('/--FILE--\s*\n(.*?)\n--EXPECT/s', $contents, $matches)) { + $this->fail($file.' FILE section missing'); + } + + return $matches[1]; + } + + private function jitProbeOk(): bool + { + $probe = $this->repoRoot.'/script/jit-runtime-probe.php'; + if (!is_file($probe)) { + return false; + } + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $proc = proc_open( + array_merge(LlvmToolchain::envPrefix($this->repoRoot), [PHP_BINARY, $probe]), + $descriptorSpec, + $pipes, + $this->repoRoot, + $this->llvmProcessEnv() + ); + if (!is_resource($proc)) { + return false; + } + fclose($pipes[0]); + fclose($pipes[1]); + fclose($pipes[2]); + + return 0 === proc_close($proc); + } + + /** + * @return array + */ + private function llvmProcessEnv(): array + { + $env = $_ENV; + foreach ($_SERVER as $key => $value) { + if (is_string($value)) { + $env[$key] = $value; + } + } + LlvmToolchain::applyProcessEnv($env, $this->repoRoot); + + return $env; + } +} diff --git a/test/unit/TryCatchComplianceTest.php b/test/unit/TryCatchComplianceTest.php index 0c35e292df1..bb47ee8b5f4 100644 --- a/test/unit/TryCatchComplianceTest.php +++ b/test/unit/TryCatchComplianceTest.php @@ -60,6 +60,63 @@ class Other {} ); } + public function testFinallyRunsBeforeCatchOnThrow(): void + { + $this->assertVmOutput( + 'assertVmOutput( + 'parseAndCompile( + 'run($block); + $this->fail('expected uncaught exception'); + } catch (\Exception) { + // finally must run before the VM maps the throw to a native exception + } + $this->assertSame("finally\n", ob_get_clean(), 'VM stdout'); + } + public function testUncaughtThrowNonZeroExit(): void { $bin = realpath(__DIR__ . '/../../bin/vm.php');