diff --git a/lib/JIT/BoundMethodCallableHelper.php b/lib/JIT/BoundMethodCallableHelper.php index 2c9fd9fc071..c4b4b8268eb 100644 --- a/lib/JIT/BoundMethodCallableHelper.php +++ b/lib/JIT/BoundMethodCallableHelper.php @@ -82,6 +82,74 @@ public static function resolveBoundMethodReceiverOperand(Block $block, int $call return self::resolveObjectOperandRoot($block, $receiver); } + /** + * Enum case FCC receivers (`E::A->f(...)`) use TYPE_CLASS_CONST_FETCH; infer enum FQCN (#6845). + */ + public static function resolveBoundMethodReceiverClassName(Block $block, int $calleeSlot): ?string + { + $arraySlot = self::resolveBoundMethodArrayRootSlot($block, $calleeSlot); + if (null === $arraySlot) { + return null; + } + foreach ($block->opCodes as $op) { + if (OpCode::TYPE_INIT_ARRAY !== $op->type || $op->arg1 !== $arraySlot || null === $op->arg2) { + continue; + } + + return self::classNameFromReceiverSlot($block, (int) $op->arg2); + } + foreach ($block->parents as $parent) { + if (!$parent instanceof Block) { + continue; + } + $name = self::resolveBoundMethodReceiverClassName($parent, $calleeSlot); + if (null !== $name) { + return $name; + } + } + + return null; + } + + private static function classNameFromReceiverSlot(Block $block, int $slot, array &$visited = []): ?string + { + if (isset($visited[$slot])) { + return null; + } + $visited[$slot] = true; + foreach ($block->opCodes as $op) { + if (OpCode::TYPE_CLASS_CONST_FETCH === $op->type && $op->arg1 === $slot) { + $classOp = $block->getOperand($op->arg2); + if ($classOp instanceof Operand\Literal) { + return (string) $classOp->value; + } + } + } + foreach ($block->opCodes as $op) { + if (OpCode::TYPE_ASSIGN !== $op->type) { + continue; + } + if ($op->arg2 !== $slot && $op->arg1 !== $slot) { + continue; + } + $resolved = self::classNameFromReceiverSlot($block, (int) $op->arg3, $visited); + if (null !== $resolved) { + return $resolved; + } + } + foreach ($block->parents as $parent) { + if (!$parent instanceof Block) { + continue; + } + $resolved = self::classNameFromReceiverSlot($parent, $slot, $visited); + if (null !== $resolved) { + return $resolved; + } + } + + return null; + } + /** * php-cfg often uses temporaries for FCC receivers; follow assigns back to $obj (#4040). */ diff --git a/lib/JIT/FromCallableHelper.php b/lib/JIT/FromCallableHelper.php index 2c7a092c057..cb51da09cdb 100644 --- a/lib/JIT/FromCallableHelper.php +++ b/lib/JIT/FromCallableHelper.php @@ -29,7 +29,9 @@ public static function createClosureVariable(Context $context, Block $block, OpC if (null !== $methodLc) { $receiverOp = BoundMethodCallableHelper::resolveBoundMethodReceiverOperand($block, $callableSlot); if (null !== $receiverOp) { - return self::fromBoundMethodCallable($context, $block, $receiverOp, $methodLc); + $classHint = BoundMethodCallableHelper::resolveBoundMethodReceiverClassName($block, $callableSlot); + + return self::fromBoundMethodCallable($context, $block, $receiverOp, $methodLc, $classHint); } } @@ -72,15 +74,17 @@ private static function fromBoundMethodCallable( Context $context, Block $block, \PHPCfg\Operand $receiverOp, - string $methodLc + string $methodLc, + ?string $classHint = null ): Variable { $className = $receiverOp->type?->userType + ?? $classHint ?? ($context->scope->className !== '' ? $context->scope->className : 'object'); $declaringClassLc = strtolower(ltrim((string) $className, '\\')); $proxyName = self::resolveInstanceProxyName($context, $declaringClassLc, $methodLc, $className); $inner = $context->resolveFunctionProxy($proxyName); $receiverVar = $context->getVariableFromOp($receiverOp); - $scopeName = $receiverOp->type?->userType ?? $className; + $scopeName = $receiverOp->type?->userType ?? $classHint ?? $className; $scopeConst = $context->context->constString((string) $scopeName, true); $boundScope = new Variable( $context, diff --git a/lib/VM/ClosureSupport.php b/lib/VM/ClosureSupport.php index 95c60cb2c71..969fa911aae 100644 --- a/lib/VM/ClosureSupport.php +++ b/lib/VM/ClosureSupport.php @@ -308,6 +308,14 @@ private static function fromArrayCallable(Context $ctx, Frame $frame, Variable $ if (Variable::TYPE_OBJECT === $receiver->type) { return self::fromInstanceMethodCallable($ctx, $frame, $receiver, $methodName); } + if (Variable::TYPE_ENUM_CASE === $receiver->type) { + return self::fromInstanceMethodCallable( + $ctx, + $frame, + EnumCaseSupport::receiverForInstanceMethod($receiver), + $methodName + ); + } if (Variable::TYPE_STRING === $receiver->type) { return self::fromStaticStringCallable( $ctx, diff --git a/test/compliance/cases/language/enum_case_method_first_class_callable.phpt b/test/compliance/cases/language/enum_case_method_first_class_callable.phpt new file mode 100644 index 00000000000..07c7e02f68f --- /dev/null +++ b/test/compliance/cases/language/enum_case_method_first_class_callable.phpt @@ -0,0 +1,12 @@ +--TEST-- +Language: enum case method first-class callable E::A->f(...) (#6845, zend_closures.c) +--FILE-- +f(...); +echo $c(), "\n"; +--EXPECT-- +a diff --git a/test/repro/maintainer_enum_case_first_class_callable.php b/test/repro/maintainer_enum_case_first_class_callable.php new file mode 100644 index 00000000000..4bb9f17d6f1 --- /dev/null +++ b/test/repro/maintainer_enum_case_first_class_callable.php @@ -0,0 +1,8 @@ +f(...); +echo $c(), "\n"; diff --git a/test/unit/FirstClassCallableTest.php b/test/unit/FirstClassCallableTest.php index f30d36422fe..8cc75b03c4f 100644 --- a/test/unit/FirstClassCallableTest.php +++ b/test/unit/FirstClassCallableTest.php @@ -91,6 +91,25 @@ public function add(int $a, int $b): int { return $a + $b; } $this->assertSame('5', ob_get_clean()); } + /** Issue #6845: enum case instance method first-class callable (E::A->f(...)). */ + public function testVmEnumCaseMethodFirstClassCallable(): void + { + $code = <<<'PHP' +f(...); +echo $c(); +PHP; + $rt = new Runtime(); + $block = $rt->parseAndCompile($code, 'test.php'); + ob_start(); + $rt->run($block); + $this->assertSame('a', ob_get_clean()); + } + /** Issue #4957: TypeReconstructor must not call missing Type::array(). */ public function testVmInstanceMethodFirstClassCallableOnNewExpression(): void {