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
78 changes: 66 additions & 12 deletions lib/VM.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,23 @@ public function invokeClosure(ClosureState $closureState, Variable ...$args): Va
{
$savedStack = $this->context->swapRunStack(null);
try {
$child = $closureState->func->getFrame($this->context, null);
$this->bindClosureCallCaptures($child, $closureState);
$init = new Frame(null, $closureState->func->block, null);
$init->vmContext = $this->context;
$this->initClosureCall($init, $closureState);
if (null === $init->call) {
throw new \LogicException('Closure invocation failed in this compiler build');
}
$child = $init->call->getFrame($this->context, !empty($init->callArgs) ? $init : null);
$this->applyClosureBinding($child, $closureState);
$child->calledArgs = $args;
$out = new Variable();
$child->returnVar = $out;
if ($child->hasHandler()) {
$child->vmContext = $this->context;
$child->handler->execute($child);

return $out->resolveIndirect();
}
$this->context->push($child);
$result = $this->runFrames();
if (self::SUCCESS !== $result) {
Expand Down Expand Up @@ -673,10 +685,7 @@ private function runFrames(): int
if (Variable::TYPE_OBJECT === $callee->type) {
$closureState = $callee->toObject()->closureState;
if (null !== $closureState) {
$frame->call = $closureState->func;
$frame->closureCall = $closureState;
$frame->callArgs = [];
$frame->callArgEntries = [];
$this->initClosureCall($frame, $closureState);
break;
}
$this->initMethodCall($frame, $callee, '__invoke');
Expand Down Expand Up @@ -754,8 +763,11 @@ private function runFrames(): int
$frame->callArgEntries = [];
break;
}
$new = $frame->call->getFrame($this->context, $frame);
$this->bindClosureCallCaptures($new, $frame->closureCall);
$new = $frame->call->getFrame(
$this->context,
!empty($frame->callArgs) ? $frame : null
);
$this->applyClosureBinding($new, $frame->closureCall);
$frame->closureCall = null;
$new->calledClass = $this->inferCalledClass($frame);
$new->returnVar = null;
Expand Down Expand Up @@ -1795,6 +1807,48 @@ protected function bindClosureCallCaptures(Frame $callee, ?ClosureState $closure
}
}

protected function initClosureCall(Frame $frame, ClosureState $state): void
{
if (null !== $state->methodName && null !== $state->methodReceiver) {
if (null !== $state->boundScopeClass && '' !== $state->boundScopeClass) {
$frame->calledClass = $state->boundScopeClass;
}
$this->initMethodCall($frame, $state->methodReceiver, $state->methodName);
$frame->closureCall = null;

return;
}
if (null !== $state->wrappedFunc) {
$frame->call = $state->wrappedFunc;
$frame->closureCall = null;
$frame->callArgs = [];
$frame->callArgEntries = [];

return;
}
$frame->call = $state->func;
$frame->closureCall = $state;
$frame->callArgs = [];
$frame->callArgEntries = [];
}

protected function applyClosureBinding(Frame $callee, ?ClosureState $closureState): void
{
$this->bindClosureCallCaptures($callee, $closureState);
if (null === $closureState) {
return;
}
if (null !== $closureState->boundThis) {
$thisIdx = $closureState->func->block->slotIndexForVariableName('this');
if (null !== $thisIdx) {
$callee->scope[$thisIdx] = $closureState->boundThis;
}
}
if (null !== $closureState->boundScopeClass && '' !== $closureState->boundScopeClass) {
$callee->calledClass = $closureState->boundScopeClass;
}
}

protected function resolveStaticClassName(string $className, Frame $frame): string
{
return $this->resolveClassScopeName($className, $frame);
Expand Down Expand Up @@ -1866,10 +1920,7 @@ protected function initMethodCall(Frame $frame, Variable $receiver, string $meth
$methodLc = strtolower($methodName);
$object = $receiver->toObject();
if (null !== $object->closureState && '__invoke' === $methodLc) {
$frame->call = $object->closureState->func;
$frame->closureCall = $object->closureState;
$frame->callArgs = [];
$frame->callArgEntries = [];
$this->initClosureCall($frame, $object->closureState);

return;
}
Expand All @@ -1882,6 +1933,9 @@ protected function initMethodCall(Frame $frame, Variable $receiver, string $meth
if (null !== $frame->block->func && null !== $frame->block->func->class) {
$callerClassLc = strtolower($frame->block->func->class->value);
}
if (null === $callerClassLc && null !== $frame->calledClass && '' !== $frame->calledClass) {
$callerClassLc = strtolower($frame->calledClass);
}
MethodVisibility::assertCallable(
$vis,
$callerClassLc,
Expand Down
54 changes: 54 additions & 0 deletions lib/VM/Builtin/ClosureBindTo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\VM\Builtin;

use PHPCompiler\Frame;
use PHPCompiler\VM\ClosureSupport;
use PHPCompiler\VM\Variable;

/** Closure::bindTo() — VM (#3266). */
final class ClosureBindTo extends VmClassMethod
{
public function __construct()
{
parent::__construct('bindTo');
}

public function execute(Frame $frame): void
{
if (\count($frame->calledArgs) < 2) {
throw new \LogicException('Closure::bindTo() expects at least 2 arguments');
}
if (null === $frame->vmContext) {
throw new \LogicException('Closure::bindTo() requires VM context');
}
$receiver = $frame->calledArgs[0]->resolveIndirect();
if (Variable::TYPE_OBJECT !== $receiver->type) {
throw new \LogicException('Closure::bindTo() called without $this');
}
$state = ClosureSupport::requireClosureState(
$receiver->toObject(),
'Closure::bindTo()'
);
$newScope = $frame->calledArgs[2] ?? null;
$bound = ClosureSupport::bindTo(
$frame->vmContext,
$state,
$frame->calledArgs[1],
$newScope
);
if (null === $frame->returnVar) {
return;
}
if (null === $bound) {
$frame->returnVar->null();

return;
}
$ret = new Variable(Variable::TYPE_OBJECT);
$ret->object($bound);
$frame->returnVar->copyFrom($ret);
}
}
38 changes: 38 additions & 0 deletions lib/VM/Builtin/ClosureFromCallable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\VM\Builtin;

use PHPCompiler\Frame;
use PHPCompiler\VM\ClosureSupport;
use PHPCompiler\VM\Variable;

/** Closure::fromCallable() — VM (#3266). */
final class ClosureFromCallable extends VmClassMethod
{
public function __construct()
{
parent::__construct('fromCallable');
}

public function execute(Frame $frame): void
{
if (\count($frame->calledArgs) < 1) {
throw new \LogicException('Closure::fromCallable() expects at least 1 argument');
}
if (null === $frame->vmContext) {
throw new \LogicException('Closure::fromCallable() requires VM context');
}
$entry = ClosureSupport::fromCallable(
$frame->vmContext,
$frame,
$frame->calledArgs[0]
);
if (null !== $frame->returnVar) {
$ret = new Variable(Variable::TYPE_OBJECT);
$ret->object($entry);
$frame->returnVar->copyFrom($ret);
}
}
}
77 changes: 76 additions & 1 deletion lib/VM/ClosureState.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use PHPCompiler\Func;

/**
* VM state for anonymous functions / closures (issue #72).
* VM state for anonymous functions / closures (issue #72, #3266).
*
* Closures are represented as objects with an attached {@see ClosureState} rather than
* registering each instance as a named user function.
Expand All @@ -21,6 +21,20 @@ final class ClosureState
*/
public array $captures;

/** When set, invoke this target instead of {@see $func} (fromCallable string/static). */
public ?Func $wrappedFunc = null;

/** When set, invoke as instance method on {@see $methodReceiver}. */
public ?Variable $methodReceiver = null;

public ?string $methodName = null;

/** Rebound $this from bindTo / fromCallable instance method wrapper. */
public ?Variable $boundThis = null;

/** Class scope name for private/protected access (bindTo / fromCallable). */
public ?string $boundScopeClass = null;

/**
* @param list<array{slot: int, var: Variable, byRef: bool}> $captures
*/
Expand All @@ -31,9 +45,62 @@ public function __construct(
$this->captures = $captures;
}

public static function fromWrappedFunc(Func $func): self
{
$stub = new Func\PHP('{closure}', new \PHPCompiler\Block(null));
$state = new self($stub);
$state->wrappedFunc = $func;

return $state;
}

public static function fromMethodCallable(Func $func, Variable $receiver, string $methodName): self
{
$stub = new Func\PHP('{closure}', new \PHPCompiler\Block(null));
$state = new self($stub);
$state->wrappedFunc = $func;
$state->methodReceiver = $receiver;
$state->methodName = $methodName;

return $state;
}

public function cloneForBind(): self
{
$captures = [];
foreach ($this->captures as $capture) {
$stored = new Variable();
if ($capture['byRef']) {
$stored->indirect($capture['var']->resolveIndirect());
} else {
$stored->copyFrom($capture['var']);
}
$captures[] = [
'slot' => $capture['slot'],
'var' => $stored,
'byRef' => $capture['byRef'],
];
}
$clone = new self($this->func, $captures);
$clone->boundThis = null !== $this->boundThis ? $this->copyVar($this->boundThis) : null;
$clone->boundScopeClass = $this->boundScopeClass;

return $clone;
}

public function isUserClosure(): bool
{
return null === $this->wrappedFunc && null === $this->methodName;
}

public static function register(Context $ctx): void
{
$entry = new ClassEntry('Closure');
$pub = \PHPCfg\Func::FLAG_PUBLIC;
$entry->methods['fromcallable'] = new Builtin\ClosureFromCallable();
$entry->methodVisibility['fromcallable'] = $pub;
$entry->methods['bindto'] = new Builtin\ClosureBindTo();
$entry->methodVisibility['bindto'] = $pub;
$ctx->classes['closure'] = $entry;
}

Expand All @@ -45,4 +112,12 @@ public function wrapObject(Context $ctx): ObjectEntry

return $entry;
}

private function copyVar(Variable $src): Variable
{
$copy = new Variable();
$copy->copyFrom($src->resolveIndirect());

return $copy;
}
}
Loading