Skip to content

[RFC] Closure self-reference #11118

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions Zend/Optimizer/dce.c
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ static inline bool may_have_side_effects(
case ZEND_FUNC_NUM_ARGS:
case ZEND_FUNC_GET_ARGS:
case ZEND_ARRAY_KEY_EXISTS:
case ZEND_BIND_SELF_REFERENCE:
/* No side effects */
return 0;
case ZEND_ADD_ARRAY_ELEMENT:
Expand Down
1 change: 1 addition & 0 deletions Zend/Optimizer/zend_inference.c
Original file line number Diff line number Diff line change
Expand Up @@ -4363,6 +4363,7 @@ static void zend_mark_cv_references(const zend_op_array *op_array, const zend_sc
case ZEND_SEND_REF:
case ZEND_SEND_VAR_EX:
case ZEND_SEND_FUNC_ARG:
case ZEND_BIND_SELF_REFERENCE:
break;
case ZEND_INIT_ARRAY:
case ZEND_ADD_ARRAY_ELEMENT:
Expand Down
1 change: 1 addition & 0 deletions Zend/Optimizer/zend_ssa.c
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,7 @@ static zend_always_inline int _zend_ssa_rename_op(const zend_op_array *op_array,
case ZEND_FETCH_DIM_FUNC_ARG:
case ZEND_FETCH_DIM_UNSET:
case ZEND_FETCH_LIST_W:
case ZEND_BIND_SELF_REFERENCE:
if (opline->op1_type == IS_CV) {
goto add_op1_def;
}
Expand Down
25 changes: 25 additions & 0 deletions Zend/tests/closures/closure_self_reference_arrow.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
--TEST--
Testing Closure self-reference functionality: Arrow functions
--FILE--
<?php

$factorial = fn(int $num) as $fn : int => $num > 1 ? $num * $fn($num - 1) : $num;

echo "5! = ", $factorial(5), PHP_EOL;


$fn = fn() as $a => fn(int $b) as $a => $a;

echo new ReflectionParameter($fn()(1), 'b'), PHP_EOL;


$c = 123;
$fn = fn() as $c => fn() => $c;

echo get_class($fn()()), PHP_EOL;

?>
--EXPECTF--
5! = 120
Parameter #0 [ <required> int $b ]
Closure
40 changes: 40 additions & 0 deletions Zend/tests/closures/closure_self_reference_basic.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
--TEST--
Testing Closure self-reference functionality: Basic
--FILE--
<?php

$factorial = function(int $num) as $factorial {
if($num > 1) {
return $num * $factorial($num - 1);
}
return $num;
};

echo "4! = ", $factorial(4), PHP_EOL;

$dynamic = function(bool $root = false) as $test use(&$dynamic) {
if ($root) {
$dynamic = function() {
return "Hello";
};
return $test();
} else {
return "Hi";
}
};

echo $dynamic(true), PHP_EOL;
echo $dynamic(true), PHP_EOL;

echo (function() as $fn : string {
echo get_class($fn), PHP_EOL;
return "Bye";
})(), PHP_EOL;

?>
--EXPECTF--
4! = 24
Hi
Hello
Closure
Bye
10 changes: 10 additions & 0 deletions Zend/tests/closures/closure_self_reference_error_arrow_param.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
--TEST--
Closure self-reference can't reuse parameter variable name (arrow function)
--FILE--
<?php

fn(int $a) as $a => 1;

?>
--EXPECTF--
Fatal error: Cannot use lexical variable $a as self reference in %s on line %d
10 changes: 10 additions & 0 deletions Zend/tests/closures/closure_self_reference_error_arrow_this.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
--TEST--
Closure self-reference can't use this as a variable name (arrow function)
--FILE--
<?php

fn() as $this => 1;

?>
--EXPECTF--
Fatal error: Cannot use $this as self reference in %s on line %d
10 changes: 10 additions & 0 deletions Zend/tests/closures/closure_self_reference_error_bind.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
--TEST--
Closure self-reference can't reuse static variable name
--FILE--
<?php

function() as $a use($a) {};

?>
--EXPECTF--
Fatal error: Cannot use lexical variable $a as self reference in %s on line %d
10 changes: 10 additions & 0 deletions Zend/tests/closures/closure_self_reference_error_param.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
--TEST--
Closure self-reference can't reuse parameter variable name
--FILE--
<?php

function(int $a) as $a {};

?>
--EXPECTF--
Fatal error: Cannot use lexical variable $a as self reference in %s on line %d
10 changes: 10 additions & 0 deletions Zend/tests/closures/closure_self_reference_error_this.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
--TEST--
Closure self-reference can't use this as a variable name
--FILE--
<?php

function() as $this {};

?>
--EXPECTF--
Fatal error: Cannot use $this as self reference in %s on line %d
23 changes: 23 additions & 0 deletions Zend/tests/closures/closure_self_reference_generator.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
--TEST--
Testing Closure self-reference functionality: Generator
--FILE--
<?php

$fn = function(int $max) as $fn {
yield from range($max, 0, -1);
if ($max > 1) {
yield from $fn($max - 1);
}
};
print_r(iterator_to_array($fn(2), false));

?>
--EXPECT--
Array
(
[0] => 2
[1] => 1
[2] => 0
[3] => 1
[4] => 0
)
26 changes: 26 additions & 0 deletions Zend/tests/closures/closure_self_reference_instances.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
--TEST--
Testing Closure self-reference functionality: Closure instances
--FILE--
<?php

$map = new WeakMap();

$fn = function() as $fn use ($map) {
var_dump(isset($map[$fn]));
};
$map[$fn] = 1;
$fn();
$fn->call(new class {});
$fn();

var_dump(count($map));
unset($fn);
var_dump(count($map));

?>
--EXPECT--
bool(true)
bool(false)
bool(true)
int(1)
int(0)
32 changes: 32 additions & 0 deletions Zend/tests/closures/closure_self_reference_this.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
--TEST--
Testing Closure self-reference functionality: This binding
--FILE--
<?php

$closure = null;
$fn = function(bool $root = false, bool $set = false) as $fn use(&$closure) : void {
echo $this::A, PHP_EOL;
if ($set) {
$closure = $fn;
}
if ($root) {
$fn();
}
return;
};
$fn = $fn->bindTo(new class{const A="bind";});
$fn(true, true);
$fn->call(new class{const A="call";}, true, true);
$fn(true, false);
$closure(true, false);

?>
--EXPECTF--
bind
bind
call
call
bind
bind
call
call
6 changes: 4 additions & 2 deletions Zend/zend_ast.c
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_class_const_or_name(zend_ast *

ZEND_API zend_ast *zend_ast_create_decl(
zend_ast_kind kind, uint32_t flags, uint32_t start_lineno, zend_string *doc_comment,
zend_string *name, zend_ast *child0, zend_ast *child1, zend_ast *child2, zend_ast *child3, zend_ast *child4
zend_string *name, zend_ast *child0, zend_ast *child1, zend_ast *child2, zend_ast *child3, zend_ast *child4, zend_ast *child5
) {
zend_ast_decl *ast;

Expand All @@ -128,6 +128,7 @@ ZEND_API zend_ast *zend_ast_create_decl(
ast->child[2] = child2;
ast->child[3] = child3;
ast->child[4] = child4;
ast->child[5] = child5;

return (zend_ast *) ast;
}
Expand Down Expand Up @@ -1165,7 +1166,8 @@ ZEND_API void ZEND_FASTCALL zend_ast_destroy(zend_ast *ast)
zend_ast_destroy(decl->child[1]);
zend_ast_destroy(decl->child[2]);
zend_ast_destroy(decl->child[3]);
ast = decl->child[4];
zend_ast_destroy(decl->child[4]);
ast = decl->child[5];
goto tail_call;
}
}
Expand Down
4 changes: 2 additions & 2 deletions Zend/zend_ast.h
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ typedef struct _zend_ast_decl {
uint32_t flags;
zend_string *doc_comment;
zend_string *name;
zend_ast *child[5];
zend_ast *child[6];
} zend_ast_decl;

typedef void (*zend_ast_process_t)(zend_ast *ast);
Expand Down Expand Up @@ -294,7 +294,7 @@ ZEND_API zend_ast * ZEND_FASTCALL zend_ast_list_add(zend_ast *list, zend_ast *op

ZEND_API zend_ast *zend_ast_create_decl(
zend_ast_kind kind, uint32_t flags, uint32_t start_lineno, zend_string *doc_comment,
zend_string *name, zend_ast *child0, zend_ast *child1, zend_ast *child2, zend_ast *child3, zend_ast *child4
zend_string *name, zend_ast *child0, zend_ast *child1, zend_ast *child2, zend_ast *child3, zend_ast *child4, zend_ast *child5
);

typedef struct {
Expand Down
2 changes: 1 addition & 1 deletion Zend/zend_closures.c
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ ZEND_METHOD(Closure, call)
ZVAL_UNDEF(&closure_result);
fci.retval = &closure_result;

if (closure->func.common.fn_flags & ZEND_ACC_GENERATOR) {
if (closure->func.common.fn_flags & (ZEND_ACC_GENERATOR | ZEND_ACC_SELF_REFERENCE)) {
zval new_closure;
zend_create_closure(&new_closure, &closure->func, newclass, closure->called_scope, newthis);
closure = (zend_closure *) Z_OBJ(new_closure);
Expand Down
43 changes: 41 additions & 2 deletions Zend/zend_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -7195,7 +7195,7 @@ static void find_implicit_binds_recursively(closure_info *info, zend_ast *ast) {
}
}

static void find_implicit_binds(closure_info *info, zend_ast *params_ast, zend_ast *stmt_ast)
static void find_implicit_binds(closure_info *info, zend_ast *params_ast, zend_ast *stmt_ast, zend_ast *as_ast)
{
zend_ast_list *param_list = zend_ast_get_list(params_ast);
uint32_t i;
Expand All @@ -7209,6 +7209,9 @@ static void find_implicit_binds(closure_info *info, zend_ast *params_ast, zend_a
zend_ast *param_ast = param_list->child[i];
zend_hash_del(&info->uses, zend_ast_get_str(param_ast->child[1]));
}
if (as_ast) {
zend_hash_del(&info->uses, zend_ast_get_str(as_ast));
}
}

static void compile_implicit_lexical_binds(
Expand Down Expand Up @@ -7272,6 +7275,36 @@ static void zend_compile_closure_uses(zend_ast *ast) /* {{{ */
}
/* }}} */

static void zend_compile_closure_self_reference(zend_ast *ast) /* {{{ */
{
zend_op_array* op_array = CG(active_op_array);

zend_string *var_name = zend_ast_get_str(ast);

CG(zend_lineno) = zend_ast_get_lineno(ast);

{
int i;
for (i = 0; i < op_array->last_var; i++) {
if (zend_string_equals(op_array->vars[i], var_name)) {
zend_error_noreturn(E_COMPILE_ERROR,
"Cannot use lexical variable $%s as self reference", ZSTR_VAL(var_name));
}
}
}

if (zend_string_equals_literal(var_name, "this")) {
zend_error_noreturn(E_COMPILE_ERROR, "Cannot use $this as self reference");
}

zend_op *opline;

opline = zend_emit_op(NULL, ZEND_BIND_SELF_REFERENCE, NULL, NULL);
opline->op1_type = IS_CV;
opline->op1.var = lookup_cv(var_name);
}
/* }}} */

static void zend_compile_implicit_closure_uses(closure_info *info)
{
zend_string *var_name;
Expand Down Expand Up @@ -7433,6 +7466,7 @@ static void zend_compile_func_decl(znode *result, zend_ast *ast, bool toplevel)
zend_ast *uses_ast = decl->child[1];
zend_ast *stmt_ast = decl->child[2];
zend_ast *return_type_ast = decl->child[3];
zend_ast *as_ast = decl->child[5];
bool is_method = decl->kind == ZEND_AST_METHOD;
zend_string *lcname;

Expand Down Expand Up @@ -7467,7 +7501,7 @@ static void zend_compile_func_decl(znode *result, zend_ast *ast, bool toplevel)
} else {
lcname = zend_begin_func_decl(result, op_array, decl, toplevel);
if (decl->kind == ZEND_AST_ARROW_FUNC) {
find_implicit_binds(&info, params_ast, stmt_ast);
find_implicit_binds(&info, params_ast, stmt_ast, as_ast);
compile_implicit_lexical_binds(&info, result, op_array);
} else if (uses_ast) {
zend_compile_closure_binding(result, op_array, uses_ast);
Expand Down Expand Up @@ -7520,6 +7554,11 @@ static void zend_compile_func_decl(znode *result, zend_ast *ast, bool toplevel)
zend_compile_closure_uses(uses_ast);
}

if (as_ast) {
zend_compile_closure_self_reference(as_ast);
op_array->fn_flags |= ZEND_ACC_SELF_REFERENCE;
}

if (ast->kind == ZEND_AST_ARROW_FUNC) {
bool needs_return = true;
if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) {
Expand Down
5 changes: 4 additions & 1 deletion Zend/zend_compile.h
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ typedef struct _zend_oparray_context {
/* Class cannot be serialized or unserialized | | | */
#define ZEND_ACC_NOT_SERIALIZABLE (1 << 29) /* X | | | */
/* | | | */
/* Function Flags (unused: 28-30) | | | */
/* Function Flags (unused: 29-30) | | | */
/* ============== | | | */
/* | | | */
/* deprecation flag | | | */
Expand Down Expand Up @@ -364,6 +364,9 @@ typedef struct _zend_oparray_context {
#define ZEND_ACC_COMPILE_TIME_EVAL (1 << 27) /* | X | | */
/* | | | */
/* op_array uses strict mode types | | | */
#define ZEND_ACC_SELF_REFERENCE (1 << 28) /* | X | | */
/* | | | */
/* op_array uses strict mode types | | | */
#define ZEND_ACC_STRICT_TYPES (1U << 31) /* | X | | */


Expand Down
Loading