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
11 changes: 9 additions & 2 deletions lib/JIT.php
Original file line number Diff line number Diff line change
Expand Up @@ -6141,6 +6141,15 @@ private function compileBlockInternal(
$callProxy,
$internalName
);
$isStaticClosure = null !== $op->block1->func
&& (($op->block1->func->flags ?? 0) & \PHPCfg\Func::FLAG_STATIC) !== 0;
if ($isStaticClosure) {
$closureObj->closureIsStatic = true;
JIT\ClosureBindHelper::storeStaticClosureFlag(
$this->context,
$this->context->helper->loadValue($closureObj)
);
}
if (null !== $block->func && null !== $block->func->class) {
JIT\ClosureBindHelper::ensureClosureBindingProperties($this->context);

Expand All @@ -6154,8 +6163,6 @@ private function compileBlockInternal(
);
$boundScope->compileTimeString = $scopeName;

$isStaticClosure = null !== $op->block1->func
&& (($op->block1->func->flags ?? 0) & \PHPCfg\Func::FLAG_STATIC) !== 0;
$boundThis = JIT\ClosureHelper::nullCapture($this->context);
if (!$isStaticClosure) {
$thisVar = $this->context->variableForScopedName('this');
Expand Down
132 changes: 132 additions & 0 deletions lib/JIT/ClosureBindHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace PHPCompiler\JIT;

use PHPCompiler\JIT\Builtin\ErrorRaise;
use PHPCompiler\JIT\Builtin\TypeErrorRaise;
use PHPCompiler\JIT\Call;
use PHPCompiler\JIT\Call\ClosureWithBinding;
Expand All @@ -21,6 +22,8 @@ final class ClosureBindHelper

public const BOUND_SCOPE_PROPERTY = '__closure_bound_scope';

public const IS_STATIC_PROPERTY = '__closure_is_static';

public static function registerJitMethods(Context $context): void
{
$context->functionProxies['closure::bindto'] = new Call\ClosureBindTo();
Expand All @@ -37,6 +40,9 @@ public static function ensureClosureBindingProperties(Context $context): void
if (!$objectType->hasProperty($classId, self::BOUND_SCOPE_PROPERTY)) {
$objectType->defineProperty($classId, self::BOUND_SCOPE_PROPERTY, Variable::TYPE_STRING);
}
if (!$objectType->hasProperty($classId, self::IS_STATIC_PROPERTY)) {
$objectType->defineProperty($classId, self::IS_STATIC_PROPERTY, Variable::TYPE_NATIVE_BOOL);
}
}

public static function bind(
Expand All @@ -56,6 +62,7 @@ public static function bind(
}

$inner = self::resolveInnerCall($context, $closure);
self::assertNotBindingStaticClosureToObject($context, $closure, $inner, $newThis);
if (null === $inner) {
return self::nullResult($context);
}
Expand All @@ -69,6 +76,7 @@ public static function bind(
$boundThis,
$boundScope
);
$result->closureIsStatic = $closure->closureIsStatic;

return $result;
}
Expand Down Expand Up @@ -231,10 +239,36 @@ private static function cloneClosureObject(
self::BOUND_SCOPE_PROPERTY,
$boundScope
);
$staticFlag = $context->type->object->propertyFetch(
$srcObj,
'Closure',
self::IS_STATIC_PROPERTY
);
$context->type->object->storeInstanceProperty(
$dest,
'Closure',
self::IS_STATIC_PROPERTY,
$staticFlag
);

return $dest;
}

public static function storeStaticClosureFlag(Context $context, Value $closureObj): void
{
self::ensureClosureBindingProperties($context);
$i1 = $context->getTypeFromString('int1');
$trueLit = $context->builder->load($i1->constInt(1, false));
$trueVar = new Variable($context, Variable::TYPE_NATIVE_BOOL, Variable::KIND_VALUE, $trueLit);
$trueVar->addref();
$context->type->object->storeInstanceProperty(
$closureObj,
'Closure',
self::IS_STATIC_PROPERTY,
$trueVar
);
}

private static function loadClosureObject(Context $context, Variable $closure): Value
{
if (Variable::TYPE_OBJECT === $closure->type) {
Expand Down Expand Up @@ -580,6 +614,104 @@ private static function scalarLabel(Variable $arg): string
};
}

private static function assertNotBindingStaticClosureToObject(
Context $context,
Variable $closure,
?Call $inner,
Variable $newThis
): void {
if ($closure->closureIsStatic) {
self::assertNotBindingObjectToStaticClosure($context, $newThis);

return;
}
if (Variable::TYPE_OBJECT === $closure->type || Variable::TYPE_VALUE === $closure->type) {
self::emitClosureObjectStaticBindGuard($context, $closure, $newThis);
}
}

private static function assertNotBindingObjectToStaticClosure(Context $context, Variable $newThis): void
{
if (Variable::TYPE_NULL === $newThis->type || ($newThis->isNullConstant ?? false)) {
return;
}
if (Variable::TYPE_OBJECT === $newThis->type) {
self::raiseStaticBindError($context);

return;
}
if (Variable::TYPE_VALUE === $newThis->type) {
self::emitValueBoxStaticBindGuard($context, $newThis);
}
}

private static function emitClosureObjectStaticBindGuard(
Context $context,
Variable $closure,
Variable $newThis
): void {
self::ensureClosureBindingProperties($context);
$obj = self::loadClosureObject($context, $closure);
$flag = $context->type->object->propertyFetch(
$obj,
'Closure',
self::IS_STATIC_PROPERTY
);
if (Variable::TYPE_NATIVE_BOOL !== $flag->type || Variable::KIND_VALUE !== $flag->kind) {
return;
}
$i1 = $context->getTypeFromString('int1');
$isStatic = $context->builder->icmp(
Builder::INT_NE,
$context->helper->loadValue($flag),
$i1->constInt(0, false)
);
$skipBlock = BasicBlockHelper::append($context, 'closure_bind_not_static');
$checkBlock = BasicBlockHelper::append($context, 'closure_bind_static_check');
$context->builder->branchIf($isStatic, $checkBlock, $skipBlock);
$context->builder->positionAtEnd($checkBlock);
self::assertNotBindingObjectToStaticClosure($context, $newThis);
$context->builder->positionAtEnd($skipBlock);
}

private static function raiseStaticBindError(Context $context): void
{
ErrorRaise::registerDeclarations($context);
ErrorRaise::ensureLinked($context);
ErrorRaise::emitRaise($context, 'Cannot bind static closure to object');
$context->builder->call($context->lookupFunction('abort'));
}

private static function emitValueBoxStaticBindGuard(Context $context, Variable $newThis): void
{
$ptr = JitValueBox::valuePtrFromVariable($context, $newThis);
$typeByte = self::loadValueTypeByte($context, $ptr);
$i8 = $context->getTypeFromString('int8');
$nullBlock = BasicBlockHelper::append($context, 'closure_bind_static_null');
$objBlock = BasicBlockHelper::append($context, 'closure_bind_static_obj');
$mergeBlock = BasicBlockHelper::append($context, 'closure_bind_static_merge');
$isNull = $context->builder->icmp(
Builder::INT_EQ,
$typeByte,
$i8->constInt(Variable::TYPE_NULL, false)
);
$context->builder->branchIf($isNull, $nullBlock, $objBlock);
$context->builder->positionAtEnd($nullBlock);
$context->builder->branch($mergeBlock);
$context->builder->positionAtEnd($objBlock);
$isObj = $context->builder->icmp(
Builder::INT_EQ,
$typeByte,
$i8->constInt(Variable::TYPE_OBJECT, false)
);
$okBlock = BasicBlockHelper::append($context, 'closure_bind_static_ok');
$context->builder->branchIf($isObj, $okBlock, $mergeBlock);
$context->builder->positionAtEnd($okBlock);
self::raiseStaticBindError($context);
$context->builder->branch($mergeBlock);
$context->builder->positionAtEnd($mergeBlock);
}

private static function thisArgLabel(string $context): string
{
return 'Closure::bind()' === $context ? '#2 ($newThis)' : '#1 ($newThis)';
Expand Down
3 changes: 3 additions & 0 deletions lib/JIT/Variable.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ final class Variable {
/** Native call proxy when this object is a JIT-lowered closure (#72). */
public ?Call $closureCall = null;

/** Anonymous `static function` — cannot bind $this (Zend zend_closures.c, #4613). */
public bool $closureIsStatic = false;

/** Heap {@see __generator_state__*} for JIT Generator objects (#3074). */
public ?\PHPLLVM\Value $generatorStatePtr = null;

Expand Down
15 changes: 15 additions & 0 deletions lib/VM/ClosureState.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,21 @@ public function isUserClosure(): bool
return null === $this->wrappedFunc && null === $this->methodName;
}

/** Zend zend_closure_bind(): static closures cannot receive a bound $this. */
public function isStaticClosure(): bool
{
$compilerFunc = $this->wrappedFunc ?? $this->func;
if (!$compilerFunc instanceof Func\PHP) {
return false;
}
$cfgFunc = $compilerFunc->block->func ?? null;
if (null === $cfgFunc) {
return false;
}

return (($cfgFunc->flags ?? 0) & \PHPCfg\Func::FLAG_STATIC) !== 0;
}

public static function register(Context $ctx): void
{
$entry = new ClassEntry('Closure');
Expand Down
3 changes: 3 additions & 0 deletions lib/VM/ClosureSupport.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ public static function bindTo(
if (null !== $state->wrappedFunc || null !== $state->methodName) {
return null;
}
if (Variable::TYPE_OBJECT === $newThis->type && $state->isStaticClosure()) {
throw new \Error('Cannot bind static closure to object');
}
$bound = $state->cloneForBind();
if (Variable::TYPE_NULL === $newThis->type) {
$bound->boundThis = null;
Expand Down
2 changes: 1 addition & 1 deletion test/compliance/ClosureVMTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public function setUp(): void

public static function providePHPTests(): \Generator
{
foreach (['closure_simple.phpt', 'closure_arrow.phpt', 'closure_in_array.phpt', 'closure_array_element_call.phpt', 'closure_use.phpt', 'closure_use_byref.phpt', 'closure_use_byref_mutate.phpt', 'closure_this_binding.phpt', 'closure_array_map.phpt', 'closure_from_callable.phpt', 'closure_from_callable_method.phpt', 'closure_bind_to.phpt', 'closure_bind_static.phpt', 'closure_bind_null_this_static.phpt', 'closure_bind_invalid_scope.phpt', 'static_arrow_fn.phpt', 'static_closure_fn.phpt'] as $file) {
foreach (['closure_simple.phpt', 'closure_arrow.phpt', 'closure_in_array.phpt', 'closure_array_element_call.phpt', 'closure_use.phpt', 'closure_use_byref.phpt', 'closure_use_byref_mutate.phpt', 'closure_this_binding.phpt', 'closure_array_map.phpt', 'closure_from_callable.phpt', 'closure_from_callable_method.phpt', 'closure_bind_to.phpt', 'closure_bind_static.phpt', 'closure_bind_null_this_static.phpt', 'closure_bind_invalid_scope.phpt', 'static_arrow_fn.phpt', 'static_closure_fn.phpt', 'static_closure_bind.phpt'] as $file) {
$path = __DIR__ . '/cases/language/' . $file;
$name = preg_replace('/\.phpt$/', '', $file) ?: $file;
yield $name => self::parsePHPT($path, $file);
Expand Down
37 changes: 37 additions & 0 deletions test/compliance/cases/language/static_closure_bind.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
--TEST--
language: static closure bindTo/bind must Error (issue #4613, Zend/zend_closures.c)
--FILE--
<?php
declare(strict_types=1);

class C {
public int $x = 1;
public function make(): Closure {
return static function () {
return 0;
};
}
}

$c = new C();
$fn = $c->make();
try {
$fn->bindTo($c);
echo "bindTo ok\n";
} catch (Throwable $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}

try {
Closure::bind($fn, $c, 'C');
echo "bind ok\n";
} catch (Throwable $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}

$unbound = $fn->bindTo(null);
echo $unbound === null ? "null\n" : "object\n";
--EXPECT--
Error: Cannot bind static closure to object
Error: Cannot bind static closure to object
object