Skip to content

Implement compile-time-eval for functions called with named arguments #10831

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 4 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
121 changes: 105 additions & 16 deletions Zend/Optimizer/sccp.c
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ typedef struct _sccp_ctx {
zval bot;
} sccp_ctx;

typedef struct _named_arg_pair {
zval *name;
zval *value;
} named_arg_pair;

#define TOP ((uint8_t)-1)
#define BOT ((uint8_t)-2)
#define PARTIAL_ARRAY ((uint8_t)-3)
Expand Down Expand Up @@ -744,7 +749,7 @@ static inline zend_result ct_eval_array_key_exists(zval *result, zval *op1, zval
return SUCCESS;
}

static bool can_ct_eval_func_call(zend_function *func, zend_string *name, uint32_t num_args, zval **args) {
static bool can_ct_eval_func_call(zend_function *func, zend_string *name, uint32_t num_args, zval **args, uint32_t num_named_args) {
/* Precondition: func->type == ZEND_INTERNAL_FUNCTION, this is a global function */
/* Functions setting ZEND_ACC_COMPILE_TIME_EVAL (@compile-time-eval) must always produce the same result for the same arguments,
* and have no dependence on global state (such as locales). It is okay if they throw
Expand All @@ -753,6 +758,13 @@ static bool can_ct_eval_func_call(zend_function *func, zend_string *name, uint32
/* This has @compile-time-eval in stub info and uses a macro such as ZEND_SUPPORTS_COMPILE_TIME_EVAL_FE */
return true;
}

/* Has a named argument, but dirname doesn't expect that, and checking the str_repeat case is too complex.
* The complexity is not worth it for one function which will unlikely be used with named parameters. */
if (num_named_args > 0) {
return false;
}

#ifndef ZEND_WIN32
/* On Windows this function may be code page dependent. */
if (zend_string_equals_literal(name, "dirname")) {
Expand All @@ -779,7 +791,7 @@ static bool can_ct_eval_func_call(zend_function *func, zend_string *name, uint32
* or just happened to be commonly used with constant operands in WP (need to test other
* applications as well, of course). */
static inline zend_result ct_eval_func_call(
zend_op_array *op_array, zval *result, zend_string *name, uint32_t num_args, zval **args) {
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) {
uint32_t i;
zend_function *func = zend_hash_find_ptr(CG(function_table), name);
if (!func || func->type != ZEND_INTERNAL_FUNCTION) {
Expand All @@ -791,7 +803,7 @@ static inline zend_result ct_eval_func_call(
return SUCCESS;
}

if (!can_ct_eval_func_call(func, name, num_args, args)) {
if (!can_ct_eval_func_call(func, name, num_args, args, num_named_args)) {
return FAILURE;
}

Expand All @@ -806,8 +818,11 @@ static inline zend_result ct_eval_func_call(
dummy_frame.opline = &dummy_opline;
dummy_opline.opcode = ZEND_DO_FCALL;

execute_data = safe_emalloc(num_args, sizeof(zval), ZEND_CALL_FRAME_SLOT * sizeof(zval));
memset(execute_data, 0, sizeof(zend_execute_data));
execute_data = zend_vm_stack_push_call_frame(ZEND_CALL_TOP_FUNCTION, func, num_args, func->common.scope);
execute_data->return_value = NULL;
execute_data->symbol_table = NULL;
execute_data->run_time_cache = NULL;
execute_data->extra_named_params = NULL;
execute_data->prev_execute_data = &dummy_frame;
EG(current_execute_data) = execute_data;

Expand All @@ -821,12 +836,47 @@ static inline zend_result ct_eval_func_call(
ZVAL_COPY(EX_VAR_NUM(i), args[i]);
}
ZVAL_NULL(result);
func->internal_function.handler(execute_data, result);

zend_result retval = SUCCESS;
zval *named_args_copies[3] = {NULL};
ZEND_ASSERT(num_named_args <= sizeof(named_args_copies) / sizeof(named_args_copies[0]));

for (i = 0; i < num_named_args; i++) {
uint32_t arg_num_unused;
/* Need 2 cache slots for zend_get_arg_offset_by_name() */
void *cache_slots[2] = {NULL};
zval *arg = zend_handle_named_arg(&execute_data, Z_STR_P(named_args[i].name), &arg_num_unused, cache_slots);
if (!arg) {
retval = FAILURE;
break;
}
ZVAL_COPY(arg, named_args[i].value);
named_args_copies[i] = arg;
}

if (retval == SUCCESS) {
/* Handle undef arguments in the same way as how the VM does it */
if (UNEXPECTED(ZEND_CALL_INFO(execute_data) & ZEND_CALL_MAY_HAVE_UNDEF)) {
/* Have to hackisly set the current EX() back one frame because zend_handle_undef_args()
* temporarily starts its own "fake frame" for execute_data. */
EG(current_execute_data) = &dummy_frame;
retval = zend_handle_undef_args(execute_data);
EG(current_execute_data) = execute_data;
}
if (retval == SUCCESS) {
func->internal_function.handler(execute_data, result);
}
}

for (i = 0; i < num_args; i++) {
zval_ptr_dtor_nogc(EX_VAR_NUM(i));
}
for (i = 0; i < num_named_args; i++) {
if (named_args_copies[i]) {
zval_ptr_dtor_nogc(named_args_copies[i]);
}
}

zend_result retval = SUCCESS;
if (EG(exception)) {
zval_ptr_dtor(result);
zend_clear_exception();
Expand All @@ -839,7 +889,7 @@ static inline zend_result ct_eval_func_call(
}
EG(capture_warnings_during_sccp) = 0;

efree(execute_data);
zend_vm_stack_free_call_frame(execute_data);
EG(current_execute_data) = prev_execute_data;
return retval;
}
Expand Down Expand Up @@ -1631,7 +1681,8 @@ static void sccp_visit_instr(scdf_ctx *scdf, zend_op *opline, zend_ssa_op *ssa_o
{
zend_call_info *call;
zval *name, *args[3] = {NULL};
int i;
named_arg_pair named_args[3] = {{NULL, NULL}};
unsigned int i;

if (!ctx->call_map) {
SET_RESULT_BOT(result);
Expand All @@ -1646,9 +1697,8 @@ static void sccp_visit_instr(scdf_ctx *scdf, zend_op *opline, zend_ssa_op *ssa_o
break;
}

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

i = 0;
if (call->first_named_arg.opline) {
for (zend_op *opline = call->first_named_arg.opline; opline != call->caller_call_opline; opline++, i++) {
if (opline->opcode == ZEND_CHECK_UNDEF_ARGS) {
break;
}
if ((opline->opcode != ZEND_SEND_VAL && opline->opcode != ZEND_SEND_VAR)
/* must have a name, which is a const */
|| opline->op2_type != IS_CONST
/* must not exceed the maximum number of named parameters */
|| i == sizeof(named_args) / sizeof(named_args[0])) {
SET_RESULT_BOT(result);
return;
}
zval *argument_name = get_op2_value(ctx, opline,
&ctx->scdf.ssa->ops[opline - ctx->scdf.op_array->opcodes]);
ZEND_ASSERT(Z_TYPE_P(argument_name) == IS_STRING);
zval *argument_value = get_op1_value(ctx, opline,
&ctx->scdf.ssa->ops[opline - ctx->scdf.op_array->opcodes]);
if (argument_value) {
if (IS_BOT(argument_value) || IS_PARTIAL_ARRAY(argument_value)) {
SET_RESULT_BOT(result);
return;
} else if (IS_TOP(argument_value)) {
return;
}
named_args[i].name = argument_name;
named_args[i].value = argument_value;
}
}
}

/* We didn't get a BOT argument, so value stays the same */
if (!IS_TOP(&ctx->values[ssa_op->result_def])) {
break;
}

if (ct_eval_func_call(scdf->op_array, &zv, Z_STR_P(name), call->num_args, args) == SUCCESS) {
if (ct_eval_func_call(scdf->op_array, &zv, Z_STR_P(name), call->num_args, args, named_args, i) == SUCCESS) {
SET_RESULT(result, &zv);
zval_ptr_dtor_nogc(&zv);
break;
Expand Down Expand Up @@ -2023,7 +2105,6 @@ static int remove_call(sccp_ctx *ctx, zend_op *opline, zend_ssa_op *ssa_op)
zend_ssa *ssa = ctx->scdf.ssa;
zend_op_array *op_array = ctx->scdf.op_array;
zend_call_info *call;
int i;

ZEND_ASSERT(ctx->call_map);
call = ctx->call_map[opline - op_array->opcodes];
Expand All @@ -2033,15 +2114,23 @@ static int remove_call(sccp_ctx *ctx, zend_op *opline, zend_ssa_op *ssa_op)
zend_ssa_remove_instr(ssa, call->caller_init_opline,
&ssa->ops[call->caller_init_opline - op_array->opcodes]);

for (i = 0; i < call->num_args; i++) {
int removed = 2 + call->num_args;
for (int i = 0; i < call->num_args; i++) {
zend_ssa_remove_instr(ssa, call->arg_info[i].opline,
&ssa->ops[call->arg_info[i].opline - op_array->opcodes]);
}
zend_op *named_arg = call->first_named_arg.opline;
if (named_arg) {
for (; named_arg != opline; named_arg++, removed++) {
zend_ssa_remove_instr(ssa, named_arg,
&ssa->ops[named_arg - op_array->opcodes]);
}
}

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

return call->num_args + 2;
return removed;
}

/* This is a basic DCE pass we run after SCCP. It only works on those instructions those result
Expand Down
4 changes: 3 additions & 1 deletion Zend/Optimizer/zend_call_graph.c
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,9 @@ ZEND_API void zend_analyze_calls(zend_arena **arena, zend_script *script, uint32
case ZEND_SEND_USER:
if (call_info) {
if (opline->op2_type == IS_CONST) {
call_info->named_args = 1;
if (!call_info->first_named_arg.opline) {
call_info->first_named_arg.opline = opline;
}
break;
}

Expand Down
2 changes: 1 addition & 1 deletion Zend/Optimizer/zend_call_graph.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ struct _zend_call_info {
zend_call_info *next_callee;
bool recursive;
bool send_unpack; /* Parameters passed by SEND_UNPACK or SEND_ARRAY */
bool named_args; /* Function has named arguments */
bool is_prototype; /* An overridden child method may be called */
int num_args; /* Number of arguments, excluding named and variadic arguments */
zend_send_arg_info first_named_arg; /* First named arg if function has named arguments */
zend_send_arg_info arg_info[1];
};

Expand Down
4 changes: 2 additions & 2 deletions ext/opcache/tests/opt/gh10801.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ opcache.optimization_level=0xe0
opcache
--FILE--
<?php
// Named argument case and does not do CTE as expected
// Named argument case with CTE
print_r(array_keys(array: [1 => 1], strict: true, filter_value: 0));
// Will not use named arguments and do CTE as expected
// Will not use named arguments, and must result in the same output
print_r(array_keys(array: [1 => 1], filter_value: 0, strict: true));
?>
--EXPECT--
Expand Down
114 changes: 114 additions & 0 deletions ext/opcache/tests/opt/sccp_042.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
--TEST--
SCCP 042: Optimisation for CTE calls with named arguments
--EXTENSIONS--
opcache
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.optimization_level=0xE0
opcache.opt_debug_level=0x400000
--FILE--
<?php

print_r(array_keys(array: [1 => 1], strict: true, filter_value: 0));
print_r(array_keys(array: [1 => 1], filter_value: 0, strict: true));
print_r(array_keys(strict: true, filter_value: 1, array: [1 => 1, 2 => 1, 3 => 9]));
print_r(array_keys([1 => 1, 2 => 1, 3 => 9], 1, true));

// The first one will already throw a fatal error.
// We can't put try-catch around these because then it won't optimize.
// We use opcache.opt_debug_level to show us the resulting SSA to verify the CTE code did the right thing.
print_r(array_keys([], strict: true));
print_r(array_keys(array: [], filter_value: 0, array: [1]));
print_r(array_keys(array: [], test: 0, strict: true));

// No CTE possible
$generated = mt_rand(0, 10);
print_r(array_keys(array: [$generated], filter_value: $generated, strict: true));

?>
--EXPECTF--
$_main:
; (lines=53, args=0, vars=1, tmps=19, ssa_vars=12, no_loops)
; (after dfa pass)
; %s
; return [long] RANGE[1..1]
; #0.CV0($generated) [undef, ref, any]
BB0:
; start exit lines=[0-52]
; level=0
0000 INIT_FCALL 1 %d string("print_r")
0001 SEND_VAL array(...) 1
0002 DO_ICALL
0003 INIT_FCALL 1 %d string("print_r")
0004 SEND_VAL array(...) 1
0005 DO_ICALL
0006 INIT_FCALL 1 %d string("print_r")
0007 SEND_VAL array(...) 1
0008 DO_ICALL
0009 INIT_FCALL 1 %d string("print_r")
0010 SEND_VAL array(...) 1
0011 DO_ICALL
0012 INIT_FCALL 1 %d string("print_r")
0013 INIT_FCALL 1 %d string("array_keys")
0014 SEND_VAL array(...) 1
0015 SEND_VAL bool(true) string("strict")
0016 CHECK_UNDEF_ARGS
0017 #5.V9 [array [long] of [long, string]] = DO_ICALL
0018 SEND_VAR #5.V9 [array [long] of [long, string]] 1
0019 DO_ICALL
0020 INIT_FCALL 1 %d string("print_r")
0021 INIT_FCALL 2 %d string("array_keys")
0022 SEND_VAL array(...) 1
0023 SEND_VAL int(0) 2
0024 SEND_VAL array(...) string("array")
0025 CHECK_UNDEF_ARGS
0026 #6.V11 [array [long] of [long, string]] = DO_ICALL
0027 SEND_VAR #6.V11 [array [long] of [long, string]] 1
0028 DO_ICALL
0029 INIT_FCALL 1 %d string("print_r")
0030 INIT_FCALL 1 %d string("array_keys")
0031 SEND_VAL array(...) 1
0032 SEND_VAL_EX int(0) string("test")
0033 SEND_VAL bool(true) string("strict")
0034 CHECK_UNDEF_ARGS
0035 #7.V13 [array [long] of [long, string]] = DO_ICALL
0036 SEND_VAR #7.V13 [array [long] of [long, string]] 1
0037 DO_ICALL
0038 INIT_FCALL 2 %d string("mt_rand")
0039 SEND_VAL int(0) 1
0040 SEND_VAL int(10) 2
0041 #8.V15 [long] = DO_ICALL
0042 ASSIGN #0.CV0($generated) [undef, ref, any] -> #9.CV0($generated) [ref, any] #8.V15 [long]
0043 INIT_FCALL 1 %d string("print_r")
0044 INIT_FCALL 3 %d string("array_keys")
0045 #10.T17 [array [long] of [any]] = INIT_ARRAY 1 (packed) #9.CV0($generated) [ref, any] NEXT
0046 SEND_VAL #10.T17 [array [long] of [any]] 1
0047 SEND_VAR #9.CV0($generated) [ref, any] 2
0048 SEND_VAL bool(true) 3
0049 #11.V18 [array [long] of [long, string]] = DO_ICALL
0050 SEND_VAR #11.V18 [array [long] of [long, string]] 1
0051 DO_ICALL
0052 RETURN int(1)
Array
(
)
Array
(
)
Array
(
[0] => 1
[1] => 2
)
Array
(
[0] => 1
[1] => 2
)

Fatal error: Uncaught ArgumentCountError: array_keys(): Argument #2 ($filter_value) must be passed explicitly, because the default value is not known in %s:%d
Stack trace:
#0 %s(%d): array_keys(Array, NULL, true)
#1 {main}
thrown in %s on line %d