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
68 changes: 68 additions & 0 deletions lib/JIT/BoundMethodCallableHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*/
Expand Down
10 changes: 7 additions & 3 deletions lib/JIT/FromCallableHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions lib/VM/ClosureSupport.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
Language: enum case method first-class callable E::A->f(...) (#6845, zend_closures.c)
--FILE--
<?php
enum E {
case A;
public function f(): string { return 'a'; }
}
$c = E::A->f(...);
echo $c(), "\n";
--EXPECT--
a
8 changes: 8 additions & 0 deletions test/repro/maintainer_enum_case_first_class_callable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

enum E {
case A;
public function f(): string { return 'a'; }
}
$c = E::A->f(...);
echo $c(), "\n";
19 changes: 19 additions & 0 deletions test/unit/FirstClassCallableTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'
<?php
enum E {
case A;
public function f(): string { return 'a'; }
}
$c = E::A->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
{
Expand Down