Skip to content

Commit caa9175

Browse files
committed
Implement compile-time-eval for functions called with named arguments
In GH-10811 I disabled compile-time-eval for function calls with named arguments because it was completely unsupported and thus would therefore crash or give semantically wrong results. This patch undoes the disabling and implements the necessary components to support CTE for named arguments. Since named arguments are a bit of a special case for the VM, I also had to add some special handling for the SCCP pass. I also had to tweak how the call removal code worked such that the named arguments, which are not in num_args, are also removed, as well as the CHECK_UNDEF_ARGS opline.
1 parent 5c058d7 commit caa9175

File tree

4 files changed

+217
-15
lines changed

4 files changed

+217
-15
lines changed

Zend/Optimizer/sccp.c

Lines changed: 99 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ typedef struct _sccp_ctx {
8383
zval bot;
8484
} sccp_ctx;
8585

86+
typedef struct _named_arg_pair {
87+
zval *name;
88+
zval *value;
89+
} named_arg_pair;
90+
8691
#define TOP ((uint8_t)-1)
8792
#define BOT ((uint8_t)-2)
8893
#define PARTIAL_ARRAY ((uint8_t)-3)
@@ -744,7 +749,7 @@ static inline zend_result ct_eval_array_key_exists(zval *result, zval *op1, zval
744749
return SUCCESS;
745750
}
746751

747-
static bool can_ct_eval_func_call(zend_function *func, zend_string *name, uint32_t num_args, zval **args) {
752+
static bool can_ct_eval_func_call(zend_function *func, zend_string *name, uint32_t num_args, zval **args, uint32_t num_named_args) {
748753
/* Precondition: func->type == ZEND_INTERNAL_FUNCTION, this is a global function */
749754
/* Functions setting ZEND_ACC_COMPILE_TIME_EVAL (@compile-time-eval) must always produce the same result for the same arguments,
750755
* and have no dependence on global state (such as locales). It is okay if they throw
@@ -753,6 +758,13 @@ static bool can_ct_eval_func_call(zend_function *func, zend_string *name, uint32
753758
/* This has @compile-time-eval in stub info and uses a macro such as ZEND_SUPPORTS_COMPILE_TIME_EVAL_FE */
754759
return true;
755760
}
761+
762+
/* Has a named argument, but dirname doesn't expect that, and checking the str_repeat case is too complex.
763+
* The complexity is not worth it for one function which will unlikely be used with named parameters. */
764+
if (num_named_args > 0) {
765+
return false;
766+
}
767+
756768
#ifndef ZEND_WIN32
757769
/* On Windows this function may be code page dependent. */
758770
if (zend_string_equals_literal(name, "dirname")) {
@@ -779,7 +791,7 @@ static bool can_ct_eval_func_call(zend_function *func, zend_string *name, uint32
779791
* or just happened to be commonly used with constant operands in WP (need to test other
780792
* applications as well, of course). */
781793
static inline zend_result ct_eval_func_call(
782-
zend_op_array *op_array, zval *result, zend_string *name, uint32_t num_args, zval **args) {
794+
zend_op_array *op_array, zval *result, zend_string *name, uint32_t num_args, zval **args, named_arg_pair *named_args, uint32_t num_named_args) {
783795
uint32_t i;
784796
zend_function *func = zend_hash_find_ptr(CG(function_table), name);
785797
if (!func || func->type != ZEND_INTERNAL_FUNCTION) {
@@ -791,7 +803,7 @@ static inline zend_result ct_eval_func_call(
791803
return SUCCESS;
792804
}
793805

794-
if (!can_ct_eval_func_call(func, name, num_args, args)) {
806+
if (!can_ct_eval_func_call(func, name, num_args, args, num_named_args)) {
795807
return FAILURE;
796808
}
797809

@@ -821,12 +833,47 @@ static inline zend_result ct_eval_func_call(
821833
ZVAL_COPY(EX_VAR_NUM(i), args[i]);
822834
}
823835
ZVAL_NULL(result);
824-
func->internal_function.handler(execute_data, result);
836+
837+
zend_result retval = SUCCESS;
838+
zval *named_args_copies[3] = {NULL};
839+
ZEND_ASSERT(num_named_args <= sizeof(named_args_copies) / sizeof(named_args_copies[0]));
840+
841+
for (i = 0; i < num_named_args; i++) {
842+
uint32_t arg_num_unused;
843+
/* Need 2 cache slots for zend_get_arg_offset_by_name() */
844+
void *cache_slots[2] = {NULL};
845+
zval *arg = zend_handle_named_arg(&execute_data, Z_STR_P(named_args[i].name), &arg_num_unused, cache_slots);
846+
if (!arg) {
847+
retval = FAILURE;
848+
break;
849+
}
850+
ZVAL_COPY(arg, named_args[i].value);
851+
named_args_copies[i] = arg;
852+
}
853+
854+
if (retval == SUCCESS) {
855+
/* Handle undef arguments in the same way as how the VM does it */
856+
if (UNEXPECTED(ZEND_CALL_INFO(execute_data) & ZEND_CALL_MAY_HAVE_UNDEF)) {
857+
/* Have to hackisly set the current EX() back one frame because zend_handle_undef_args()
858+
* temporarily starts its own "fake frame" for execute_data. */
859+
EG(current_execute_data) = &dummy_frame;
860+
retval = zend_handle_undef_args(execute_data);
861+
EG(current_execute_data) = execute_data;
862+
}
863+
if (retval == SUCCESS) {
864+
func->internal_function.handler(execute_data, result);
865+
}
866+
}
867+
825868
for (i = 0; i < num_args; i++) {
826869
zval_ptr_dtor_nogc(EX_VAR_NUM(i));
827870
}
871+
for (i = 0; i < num_named_args; i++) {
872+
if (named_args_copies[i]) {
873+
zval_ptr_dtor_nogc(named_args_copies[i]);
874+
}
875+
}
828876

829-
zend_result retval = SUCCESS;
830877
if (EG(exception)) {
831878
zval_ptr_dtor(result);
832879
zend_clear_exception();
@@ -1631,7 +1678,8 @@ static void sccp_visit_instr(scdf_ctx *scdf, zend_op *opline, zend_ssa_op *ssa_o
16311678
{
16321679
zend_call_info *call;
16331680
zval *name, *args[3] = {NULL};
1634-
int i;
1681+
named_arg_pair named_args[3] = {{NULL, NULL}};
1682+
unsigned int i;
16351683

16361684
if (!ctx->call_map) {
16371685
SET_RESULT_BOT(result);
@@ -1646,9 +1694,8 @@ static void sccp_visit_instr(scdf_ctx *scdf, zend_op *opline, zend_ssa_op *ssa_o
16461694
break;
16471695
}
16481696

1649-
/* We're only interested in functions with up to three arguments right now.
1650-
* Note that named arguments with the argument in declaration order will still work. */
1651-
if (call->num_args > 3 || call->send_unpack || call->is_prototype || call->named_args) {
1697+
/* We're only interested in functions with up to three positional arguments right now. */
1698+
if (call->num_args > 3 || call->send_unpack || call->is_prototype) {
16521699
SET_RESULT_BOT(result);
16531700
break;
16541701
}
@@ -1672,12 +1719,44 @@ static void sccp_visit_instr(scdf_ctx *scdf, zend_op *opline, zend_ssa_op *ssa_o
16721719
}
16731720
}
16741721

1722+
i = 0;
1723+
if (call->first_named_arg.opline) {
1724+
for (zend_op *opline = call->first_named_arg.opline; opline != call->caller_call_opline; opline++, i++) {
1725+
if (opline->opcode == ZEND_CHECK_UNDEF_ARGS) {
1726+
break;
1727+
}
1728+
if ((opline->opcode != ZEND_SEND_VAL && opline->opcode != ZEND_SEND_VAR)
1729+
/* must have a name, which is a const */
1730+
|| opline->op2_type != IS_CONST
1731+
/* must not exceed the maximum number of named parameters */
1732+
|| i == sizeof(named_args) / sizeof(named_args[0])) {
1733+
SET_RESULT_BOT(result);
1734+
return;
1735+
}
1736+
zval *argument_name = get_op2_value(ctx, opline,
1737+
&ctx->scdf.ssa->ops[opline - ctx->scdf.op_array->opcodes]);
1738+
ZEND_ASSERT(Z_TYPE_P(argument_name) == IS_STRING);
1739+
zval *argument_value = get_op1_value(ctx, opline,
1740+
&ctx->scdf.ssa->ops[opline - ctx->scdf.op_array->opcodes]);
1741+
if (argument_value) {
1742+
if (IS_BOT(argument_value) || IS_PARTIAL_ARRAY(argument_value)) {
1743+
SET_RESULT_BOT(result);
1744+
return;
1745+
} else if (IS_TOP(argument_value)) {
1746+
return;
1747+
}
1748+
named_args[i].name = argument_name;
1749+
named_args[i].value = argument_value;
1750+
}
1751+
}
1752+
}
1753+
16751754
/* We didn't get a BOT argument, so value stays the same */
16761755
if (!IS_TOP(&ctx->values[ssa_op->result_def])) {
16771756
break;
16781757
}
16791758

1680-
if (ct_eval_func_call(scdf->op_array, &zv, Z_STR_P(name), call->num_args, args) == SUCCESS) {
1759+
if (ct_eval_func_call(scdf->op_array, &zv, Z_STR_P(name), call->num_args, args, named_args, i) == SUCCESS) {
16811760
SET_RESULT(result, &zv);
16821761
zval_ptr_dtor_nogc(&zv);
16831762
break;
@@ -2023,7 +2102,6 @@ static int remove_call(sccp_ctx *ctx, zend_op *opline, zend_ssa_op *ssa_op)
20232102
zend_ssa *ssa = ctx->scdf.ssa;
20242103
zend_op_array *op_array = ctx->scdf.op_array;
20252104
zend_call_info *call;
2026-
int i;
20272105

20282106
ZEND_ASSERT(ctx->call_map);
20292107
call = ctx->call_map[opline - op_array->opcodes];
@@ -2033,15 +2111,23 @@ static int remove_call(sccp_ctx *ctx, zend_op *opline, zend_ssa_op *ssa_op)
20332111
zend_ssa_remove_instr(ssa, call->caller_init_opline,
20342112
&ssa->ops[call->caller_init_opline - op_array->opcodes]);
20352113

2036-
for (i = 0; i < call->num_args; i++) {
2114+
int removed = 2 + call->num_args;
2115+
for (int i = 0; i < call->num_args; i++) {
20372116
zend_ssa_remove_instr(ssa, call->arg_info[i].opline,
20382117
&ssa->ops[call->arg_info[i].opline - op_array->opcodes]);
20392118
}
2119+
zend_op *named_arg = call->first_named_arg.opline;
2120+
if (named_arg) {
2121+
for (; named_arg != opline; named_arg++, removed++) {
2122+
zend_ssa_remove_instr(ssa, named_arg,
2123+
&ssa->ops[named_arg - op_array->opcodes]);
2124+
}
2125+
}
20402126

20412127
// TODO: remove call_info completely???
20422128
call->callee_func = NULL;
20432129

2044-
return call->num_args + 2;
2130+
return removed;
20452131
}
20462132

20472133
/* This is a basic DCE pass we run after SCCP. It only works on those instructions those result

Zend/Optimizer/zend_call_graph.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ ZEND_API void zend_analyze_calls(zend_arena **arena, zend_script *script, uint32
125125
case ZEND_SEND_USER:
126126
if (call_info) {
127127
if (opline->op2_type == IS_CONST) {
128-
call_info->named_args = 1;
128+
if (!call_info->first_named_arg.opline) {
129+
call_info->first_named_arg.opline = opline;
130+
}
129131
break;
130132
}
131133

Zend/Optimizer/zend_call_graph.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ struct _zend_call_info {
3636
zend_call_info *next_callee;
3737
bool recursive;
3838
bool send_unpack; /* Parameters passed by SEND_UNPACK or SEND_ARRAY */
39-
bool named_args; /* Function has named arguments */
4039
bool is_prototype; /* An overridden child method may be called */
4140
int num_args; /* Number of arguments, excluding named and variadic arguments */
41+
zend_send_arg_info first_named_arg; /* First named arg if function has named arguments */
4242
zend_send_arg_info arg_info[1];
4343
};
4444

ext/opcache/tests/opt/sccp_042.phpt

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
--TEST--
2+
SCCP 042: Optimisation for CTE calls with named arguments
3+
--EXTENSIONS--
4+
opcache
5+
--INI--
6+
opcache.enable=1
7+
opcache.enable_cli=1
8+
opcache.optimization_level=0xE0
9+
opcache.opt_debug_level=0x400000
10+
--FILE--
11+
<?php
12+
13+
print_r(array_keys(array: [1 => 1], strict: true, filter_value: 0));
14+
print_r(array_keys(array: [1 => 1], filter_value: 0, strict: true));
15+
print_r(array_keys(strict: true, filter_value: 1, array: [1 => 1, 2 => 1, 3 => 9]));
16+
print_r(array_keys([1 => 1, 2 => 1, 3 => 9], 1, true));
17+
18+
// The first one will already throw a fatal error.
19+
// We can't put try-catch around these because then it won't optimize.
20+
// We use opcache.opt_debug_level to show us the resulting SSA to verify the CTE code did the right thing.
21+
print_r(array_keys([], strict: true));
22+
print_r(array_keys(array: [], filter_value: 0, array: [1]));
23+
print_r(array_keys(array: [], test: 0, strict: true));
24+
25+
// No CTE possible
26+
$generated = mt_rand(0, 10);
27+
print_r(array_keys(array: [$generated], filter_value: $generated, strict: true));
28+
29+
?>
30+
--EXPECTF--
31+
$_main:
32+
; (lines=53, args=0, vars=1, tmps=19, ssa_vars=12, no_loops)
33+
; (after dfa pass)
34+
; %s
35+
; return [long] RANGE[1..1]
36+
; #0.CV0($generated) [undef, ref, any]
37+
BB0:
38+
; start exit lines=[0-52]
39+
; level=0
40+
0000 INIT_FCALL 1 96 string("print_r")
41+
0001 SEND_VAL array(...) 1
42+
0002 DO_ICALL
43+
0003 INIT_FCALL 1 96 string("print_r")
44+
0004 SEND_VAL array(...) 1
45+
0005 DO_ICALL
46+
0006 INIT_FCALL 1 96 string("print_r")
47+
0007 SEND_VAL array(...) 1
48+
0008 DO_ICALL
49+
0009 INIT_FCALL 1 96 string("print_r")
50+
0010 SEND_VAL array(...) 1
51+
0011 DO_ICALL
52+
0012 INIT_FCALL 1 96 string("print_r")
53+
0013 INIT_FCALL 1 96 string("array_keys")
54+
0014 SEND_VAL array(...) 1
55+
0015 SEND_VAL bool(true) string("strict")
56+
0016 CHECK_UNDEF_ARGS
57+
0017 #5.V9 [array [long] of [long, string]] = DO_ICALL
58+
0018 SEND_VAR #5.V9 [array [long] of [long, string]] 1
59+
0019 DO_ICALL
60+
0020 INIT_FCALL 1 96 string("print_r")
61+
0021 INIT_FCALL 2 112 string("array_keys")
62+
0022 SEND_VAL array(...) 1
63+
0023 SEND_VAL int(0) 2
64+
0024 SEND_VAL array(...) string("array")
65+
0025 CHECK_UNDEF_ARGS
66+
0026 #6.V11 [array [long] of [long, string]] = DO_ICALL
67+
0027 SEND_VAR #6.V11 [array [long] of [long, string]] 1
68+
0028 DO_ICALL
69+
0029 INIT_FCALL 1 96 string("print_r")
70+
0030 INIT_FCALL 1 96 string("array_keys")
71+
0031 SEND_VAL array(...) 1
72+
0032 SEND_VAL_EX int(0) string("test")
73+
0033 SEND_VAL bool(true) string("strict")
74+
0034 CHECK_UNDEF_ARGS
75+
0035 #7.V13 [array [long] of [long, string]] = DO_ICALL
76+
0036 SEND_VAR #7.V13 [array [long] of [long, string]] 1
77+
0037 DO_ICALL
78+
0038 INIT_FCALL 2 112 string("mt_rand")
79+
0039 SEND_VAL int(0) 1
80+
0040 SEND_VAL int(10) 2
81+
0041 #8.V15 [long] = DO_ICALL
82+
0042 ASSIGN #0.CV0($generated) [undef, ref, any] -> #9.CV0($generated) [ref, any] #8.V15 [long]
83+
0043 INIT_FCALL 1 96 string("print_r")
84+
0044 INIT_FCALL 3 128 string("array_keys")
85+
0045 #10.T17 [array [long] of [any]] = INIT_ARRAY 1 (packed) #9.CV0($generated) [ref, any] NEXT
86+
0046 SEND_VAL #10.T17 [array [long] of [any]] 1
87+
0047 SEND_VAR #9.CV0($generated) [ref, any] 2
88+
0048 SEND_VAL bool(true) 3
89+
0049 #11.V18 [array [long] of [long, string]] = DO_ICALL
90+
0050 SEND_VAR #11.V18 [array [long] of [long, string]] 1
91+
0051 DO_ICALL
92+
0052 RETURN int(1)
93+
Array
94+
(
95+
)
96+
Array
97+
(
98+
)
99+
Array
100+
(
101+
[0] => 1
102+
[1] => 2
103+
)
104+
Array
105+
(
106+
[0] => 1
107+
[1] => 2
108+
)
109+
110+
Fatal error: Uncaught ArgumentCountError: array_keys(): Argument #2 ($filter_value) must be passed explicitly, because the default value is not known in %s:%d
111+
Stack trace:
112+
#0 %s(%d): array_keys(Array, NULL, true)
113+
#1 {main}
114+
thrown in %s on line %d

0 commit comments

Comments
 (0)