Skip to content

Commit f401390

Browse files
committed
Added support for passing default using named arguments to closures.
1 parent defe31a commit f401390

10 files changed

+780
-602
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
--TEST--
2+
Tests passing default as a named argument to a closure
3+
--FILE--
4+
<?php
5+
6+
$F = fn ($V = 1, $default = 2) => $V + $default;
7+
var_dump($F(default: default + 1));
8+
?>
9+
--EXPECT--
10+
int(4)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
--TEST--
2+
Tests passing default as a named argument to a closure that doesn't declare any argument by that name
3+
--FILE--
4+
<?php
5+
6+
$F = fn ($V = 1) => $V;
7+
var_dump($F(default: default + 1));
8+
?>
9+
--EXPECTF--
10+
Fatal error: Uncaught ValueError: Cannot pass default to non-existent named parameter $default of {closure:%s:%d}() in %s:%d
11+
Stack trace:
12+
%a
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
--TEST--
2+
Tests passing default to a named parent argument whilst calling child arguments positionally
3+
--FILE--
4+
<?php
5+
6+
$F = fn ($X = 1, $Y = 2) => $X + $Y;
7+
var_dump($F(X: $F(0, 1) + default));
8+
?>
9+
--EXPECTF--
10+
int(4)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
--TEST--
2+
Tests passing default to a named parent argument that is different from the child call's last named argument
3+
--FILE--
4+
<?php
5+
6+
$F = fn ($X = 1, $Y = 2) => $X + $Y;
7+
var_dump($F(X: $F(X: 0, Y: 1) + default));
8+
?>
9+
--EXPECTF--
10+
int(4)

Zend/zend_compile.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3703,6 +3703,7 @@ static uint32_t zend_compile_args(
37033703
bool uses_arg_unpack = 0;
37043704
uint32_t arg_count = 0; /* number of arguments not including unpacks */
37053705
uint32_t prev_arg_num = CG(context).arg_num;
3706+
zend_string *prev_arg_name = CG(context).arg_name;
37063707

37073708
/* Whether named arguments are used syntactically, to enforce language level limitations.
37083709
* May not actually use named argument passing. */
@@ -3781,6 +3782,7 @@ static uint32_t zend_compile_args(
37813782
}
37823783

37833784
CG(context).arg_num = arg_num;
3785+
CG(context).arg_name = arg_name;
37843786

37853787
/* Treat passing of $GLOBALS the same as passing a call.
37863788
* This will error at runtime if the argument is by-ref. */
@@ -3897,6 +3899,7 @@ static uint32_t zend_compile_args(
38973899
}
38983900

38993901
CG(context).arg_num = prev_arg_num;
3902+
CG(context).arg_name = prev_arg_name;
39003903

39013904
return arg_count;
39023905
}
@@ -3912,6 +3915,16 @@ static void zend_compile_default(znode *result, zend_ast *ast)
39123915

39133916
zend_op *opline = zend_emit_op_tmp(result, ZEND_FETCH_DEFAULT_ARG, NULL, NULL);
39143917
opline->op1.num = arg_num;
3918+
3919+
// Argument number could not be determined; send argument name instead.
3920+
if (arg_num == (uint32_t) -1) {
3921+
zend_string *arg_name = CG(context).arg_name;
3922+
zend_string_addref(arg_name);
3923+
3924+
opline->op1_type = IS_CONST;
3925+
opline->op1.constant = zend_add_literal_string(&arg_name);
3926+
opline->op2.num = zend_alloc_cache_slot();
3927+
}
39153928
}
39163929

39173930
ZEND_API uint8_t zend_get_call_op(const zend_op *init_op, zend_function *fbc) /* {{{ */

Zend/zend_compile.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ typedef struct _zend_oparray_context {
208208
zend_property_hook_kind active_property_hook_kind;
209209
bool in_jmp_frameless_branch;
210210
uint32_t arg_num; // Current 1-based argument index if compiling arguments, otherwise zero.
211+
zend_string *arg_name;
211212
} zend_oparray_context;
212213

213214
/* Class, property and method flags class|meth.|prop.|const*/

Zend/zend_vm_def.h

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9808,7 +9808,7 @@ ZEND_VM_HANDLER(209, ZEND_INIT_PARENT_PROPERTY_HOOK_CALL, CONST, UNUSED|NUM, NUM
98089808
ZEND_VM_NEXT_OPCODE();
98099809
}
98109810

9811-
ZEND_VM_HANDLER(210, ZEND_FETCH_DEFAULT_ARG, UNUSED|NUM, UNUSED)
9811+
ZEND_VM_HANDLER(210, ZEND_FETCH_DEFAULT_ARG, UNUSED|NUM|CONST, UNUSED|NUM)
98129812
{
98139813
USE_OPLINE
98149814
SAVE_OPLINE();
@@ -9817,20 +9817,40 @@ ZEND_VM_HANDLER(210, ZEND_FETCH_DEFAULT_ARG, UNUSED|NUM, UNUSED)
98179817
ZVAL_UNDEF(EX_VAR(opline->result.var));
98189818

98199819
zend_function *called_func = EX(call)->func;
9820-
98219820
reflection_parameter_reference param;
9822-
param.offset = opline->op1.num - 1;
9821+
9822+
zend_string *arg_name;
9823+
if (OP1_TYPE == IS_CONST) {
9824+
// Argument offset could not be determined at compile-time (i.e. closure named param); look up offset by name.
9825+
arg_name = Z_STR_P(RT_CONSTANT(opline, opline->op1));
9826+
param.offset = zend_get_arg_offset_by_name(called_func, arg_name, CACHE_ADDR(opline->op2.num));
9827+
} else {
9828+
param.offset = opline->op1.num - 1;
9829+
}
9830+
98239831
param.required = param.offset < called_func->common.required_num_args;
98249832
param.fptr = called_func;
98259833

98269834
if (UNEXPECTED(param.required || param.offset >= called_func->common.num_args)) {
9827-
zend_value_error("Cannot pass default to %s parameter %u of %s%s%s()",
9828-
param.required ? "required" : "undeclared",
9829-
opline->op1.num,
9830-
called_func->common.scope ? ZSTR_VAL(called_func->common.scope->name) : "",
9831-
called_func->common.scope ? "::" : "",
9832-
ZSTR_VAL(called_func->common.function_name)
9833-
);
9835+
if (OP1_TYPE == IS_CONST) {
9836+
zend_value_error(
9837+
"Cannot pass default to non-existent named parameter $%s of %s%s%s()",
9838+
ZSTR_VAL(arg_name),
9839+
called_func->common.scope ? ZSTR_VAL(called_func->common.scope->name) : "",
9840+
called_func->common.scope ? "::" : "",
9841+
ZSTR_VAL(called_func->common.function_name)
9842+
);
9843+
} else {
9844+
zend_value_error(
9845+
"Cannot pass default to %s parameter %u of %s%s%s()",
9846+
param.required ? "required" : "undeclared",
9847+
opline->op1.num,
9848+
called_func->common.scope ? ZSTR_VAL(called_func->common.scope->name) : "",
9849+
called_func->common.scope ? "::" : "",
9850+
ZSTR_VAL(called_func->common.function_name)
9851+
);
9852+
}
9853+
98349854
HANDLE_EXCEPTION();
98359855
}
98369856

0 commit comments

Comments
 (0)