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
9 changes: 7 additions & 2 deletions lib/Block.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];

Expand Down Expand Up @@ -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];
}
}
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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);
Expand Down
39 changes: 35 additions & 4 deletions lib/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -2021,20 +2022,40 @@ 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(
OpCode::TYPE_CLOSURE,
$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')
Expand Down Expand Up @@ -2763,8 +2784,18 @@ protected function compileBoolConstant(Block $block, bool $value): int
}

/**
* @param list<string> $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).
*/
Expand Down
3 changes: 3 additions & 0 deletions lib/Frame.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
46 changes: 39 additions & 7 deletions lib/JIT.php
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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;
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions lib/JIT/Builtin/Type/Object_.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
37 changes: 37 additions & 0 deletions lib/JIT/ClosureHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\JIT;

/**
* Minimal JIT lowering for anonymous closures without use() (issue #72).
*
* Compiles the closure CFG as a native function and wraps the result in a Closure
* object whose {@see Variable::$closureCall} proxy handles direct / __invoke calls.
*/
final class ClosureHelper
{
private static int $counter = 0;

public static function nextInternalName(): string
{
return '{closure}_'.(++self::$counter);
}

public static function resolveCall(Variable $receiver): ?Call
{
return $receiver->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;
}
}
3 changes: 3 additions & 0 deletions lib/JIT/Variable.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
7 changes: 7 additions & 0 deletions lib/OpCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<array{name: string, slot: int, byRef: bool}>
*/
public array $closureCaptures = [];

public function __construct(int $type, ?int $arg1 = null, ?int $arg2 = null, ?int $arg3 = null) {
$this->type = $type;
$this->arg1 = $arg1;
Expand Down
Loading