diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 6fd4df023520..d17e34bba418 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -5301,11 +5301,64 @@ static zend_result zend_compile_func_array_map(znode *result, zend_ast_list *arg return FAILURE; } + /* Bail out if callback is not an FCC */ zend_ast *callback = args->child[0]; if (callback->kind != ZEND_AST_CALL && callback->kind != ZEND_AST_STATIC_CALL) { return FAILURE; } + zend_ast *args_ast = zend_ast_call_get_args(callback); + if (args_ast->kind != ZEND_AST_CALLABLE_CONVERT) { + return FAILURE; + } + + /* PFAs with non-literal pre-bound arguments are not optimizable because we + * can't memoize arguments without breaking pass-by-reference. + * TODO: Support PFAs when the function is known to not receive by ref. */ + zend_ast_fcc *fcc = (zend_ast_fcc*)args_ast; + zend_ast_list *fcc_args = zend_ast_get_list(fcc->args); + for (uint32_t i = 0; i < fcc_args->children; i++) { + zend_ast *arg = fcc_args->child[i]; + if (arg->kind == ZEND_AST_NAMED_ARG) { + arg = arg->child[1]; + } + + if (arg->kind == ZEND_AST_PLACEHOLDER_ARG) { + continue; + } + + if (arg->kind != ZEND_AST_ZVAL) { + return FAILURE; + } + } + + /* Evaluate class name */ + znode class_node; + if (callback->kind == ZEND_AST_STATIC_CALL) { + znode result; + zend_compile_expr(&result, callback->child[0]); + if (result.op_type == IS_CONST) { + class_node = result; + } else { + class_node = result; + zend_emit_op_tmp(&class_node, ZEND_QM_ASSIGN, &result, NULL); + } + } else { + class_node.op_type = IS_UNUSED; + } + + /* Evaluate function name */ + znode func_node; + { + znode result; + zend_compile_expr(&result, callback->child[callback->kind == ZEND_AST_CALL ? 0 : 1]); + if (result.op_type == IS_CONST) { + func_node = result; + } else { + zend_emit_op_tmp(&func_node, ZEND_QM_ASSIGN, &result, NULL); + } + } + znode value; value.op_type = IS_TMP_VAR; value.u.op.var = get_temporary_variable(); @@ -5314,6 +5367,12 @@ static zend_result zend_compile_func_array_map(znode *result, zend_ast_list *arg zend_ast_create_znode(&value)); if (!call_args) { CG(active_op_array)->T--; + if (func_node.op_type == IS_CONST) { + zval_ptr_dtor_nogc(&func_node.u.constant); + } + if (class_node.op_type == IS_CONST) { + zval_ptr_dtor_nogc(&class_node.u.constant); + } /* The callback is not a FCC/PFA, or is not optimizable */ return FAILURE; } @@ -5353,14 +5412,35 @@ static zend_result zend_compile_func_array_map(znode *result, zend_ast_list *arg /* loop body */ znode call_result; + zend_ast *func_ast; + if (func_node.op_type == IS_CONST) { + func_ast = zend_ast_create_znode(&func_node); + } else { + znode copy_node; + zend_emit_op_tmp(©_node, ZEND_COPY_TMP, &func_node, NULL); + func_ast = zend_ast_create_znode(©_node); + } switch (callback->kind) { - case ZEND_AST_CALL: - zend_compile_expr(&call_result, zend_ast_create(ZEND_AST_CALL, callback->child[0], call_args)); + case ZEND_AST_CALL: { + zend_compile_expr(&call_result, zend_ast_create(ZEND_AST_CALL, func_ast, call_args)); break; - case ZEND_AST_STATIC_CALL: - zend_compile_expr(&call_result, zend_ast_create(ZEND_AST_STATIC_CALL, callback->child[0], callback->child[1], call_args)); + } + case ZEND_AST_STATIC_CALL: { + zend_ast *class_ast; + if (class_node.op_type == IS_CONST) { + class_ast = zend_ast_create_znode(&class_node); + } else { + znode copy_node; + zend_emit_op_tmp(©_node, ZEND_COPY_TMP, &class_node, NULL); + class_ast = zend_ast_create_znode(©_node); + } + + zend_compile_expr(&call_result, zend_ast_create(ZEND_AST_STATIC_CALL, class_ast, func_ast, call_args)); + zend_ast_destroy(class_ast); break; + } } + zend_ast_destroy(func_ast); opline = zend_emit_op(NULL, ZEND_ADD_ARRAY_ELEMENT, &call_result, &key); SET_NODE(opline->result, result); /* end loop body */ @@ -5375,6 +5455,12 @@ static zend_result zend_compile_func_array_map(znode *result, zend_ast_list *arg zend_end_loop(opnum_fetch, &reset_node); zend_emit_op(NULL, ZEND_FE_FREE, &reset_node, NULL); + if (func_node.op_type != IS_CONST) { + zend_emit_op(NULL, ZEND_FREE, &func_node, NULL); + } + if (class_node.op_type != IS_UNUSED && class_node.op_type != IS_CONST) { + zend_emit_op(NULL, ZEND_FREE, &class_node, NULL); + } return SUCCESS; } diff --git a/ext/opcache/tests/array_map_foreach_optimization_006.phpt b/ext/opcache/tests/array_map_foreach_optimization_006.phpt index 889fee27b8a2..4c557ce6802c 100644 --- a/ext/opcache/tests/array_map_foreach_optimization_006.phpt +++ b/ext/opcache/tests/array_map_foreach_optimization_006.phpt @@ -32,26 +32,30 @@ $_main: 0003 T3 = DO_ICALL 0004 ASSIGN CV0($array) T3 0005 ASSIGN CV1($plus1) string("plus1") -0006 TYPE_ASSERT 131079 string("array_map") CV0($array) -0007 T3 = INIT_ARRAY 0 (packed) NEXT -0008 V4 = FE_RESET_R CV0($array) 0015 -0009 T6 = FE_FETCH_R V4 T5 0015 -0010 INIT_DYNAMIC_CALL 1 CV1($plus1) -0011 SEND_VAL_EX T5 1 -0012 T5 = DO_FCALL -0013 T3 = ADD_ARRAY_ELEMENT T5 T6 -0014 JMP 0009 -0015 FE_FREE V4 -0016 ASSIGN CV2($foo) T3 -0017 INIT_FCALL 1 %d string("var_dump") -0018 SEND_VAR CV2($foo) 1 -0019 DO_ICALL -0020 RETURN int(1) +0006 T4 = QM_ASSIGN CV1($plus1) +0007 TYPE_ASSERT 131079 string("array_map") CV0($array) +0008 T3 = INIT_ARRAY 0 (packed) NEXT +0009 V5 = FE_RESET_R CV0($array) 0017 +0010 T7 = FE_FETCH_R V5 T6 0017 +0011 T8 = COPY_TMP T4 +0012 INIT_DYNAMIC_CALL 1 T8 +0013 SEND_VAL_EX T6 1 +0014 T6 = DO_FCALL +0015 T3 = ADD_ARRAY_ELEMENT T6 T7 +0016 JMP 0010 +0017 FE_FREE V5 +0018 FREE T4 +0019 ASSIGN CV2($foo) T3 +0020 INIT_FCALL 1 %d string("var_dump") +0021 SEND_VAR CV2($foo) 1 +0022 DO_ICALL +0023 RETURN int(1) LIVE RANGES: - 3: 0008 - 0016 (tmp/var) - 4: 0009 - 0015 (loop) - 5: 0010 - 0011 (tmp/var) - 6: 0010 - 0013 (tmp/var) + 4: 0007 - 0018 (tmp/var) + 3: 0009 - 0019 (tmp/var) + 5: 0010 - 0017 (loop) + 6: 0011 - 0013 (tmp/var) + 7: 0011 - 0015 (tmp/var) plus1: ; (lines=3, args=1, vars=1, tmps=%d) diff --git a/ext/opcache/tests/array_map_foreach_optimization_009.phpt b/ext/opcache/tests/array_map_foreach_optimization_009.phpt new file mode 100644 index 000000000000..758c3504a88f --- /dev/null +++ b/ext/opcache/tests/array_map_foreach_optimization_009.phpt @@ -0,0 +1,93 @@ +--TEST-- +array_map(): foreach optimization - dynamic-call drift bug +--CREDITS-- +Ryan @ Calif.io +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +--FILE-- +getMessage(), "\n"; +} + +echo "empty dynamic-call control\n"; +try { + var_dump($array_map($missing(...), [])); +} catch (Throwable $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +$obj = new Trusted; +echo "direct array_map static call\n"; +var_dump(array_map($obj::f(...), cand_86013_input_changes_target())); + +?> +--EXPECT-- +direct array_map +array(1) { + [0]=> + string(15) "trusted:payload" +} +dynamic-call control +array(1) { + [0]=> + string(15) "trusted:payload" +} +empty direct +array(0) { +} +empty dynamic-call control +Error: Call to undefined function cand_86013_missing() +direct array_map static call +array(1) { + [0]=> + string(15) "trusted:payload" +} diff --git a/ext/opcache/tests/array_map_foreach_optimization_010.phpt b/ext/opcache/tests/array_map_foreach_optimization_010.phpt new file mode 100644 index 000000000000..764eb22d9d60 --- /dev/null +++ b/ext/opcache/tests/array_map_foreach_optimization_010.phpt @@ -0,0 +1,68 @@ +--TEST-- +array_map(): foreach optimization - pfa pre-bound arg reexecution bug +--CREDITS-- +Ryan @ Calif.io +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +--FILE-- + +--EXPECT-- +direct array_map +BOUND:1 +INPUT +array(3) { + [0]=> + string(1) "b" + [1]=> + string(1) "b" + [2]=> + string(1) "b" +} +int(1) +dynamic-call control +BOUND:1 +INPUT +array(3) { + [0]=> + string(1) "b" + [1]=> + string(1) "b" + [2]=> + string(1) "b" +} +int(1) diff --git a/ext/opcache/tests/array_map_foreach_optimization_011.phpt b/ext/opcache/tests/array_map_foreach_optimization_011.phpt new file mode 100644 index 000000000000..e8f9f777c65e --- /dev/null +++ b/ext/opcache/tests/array_map_foreach_optimization_011.phpt @@ -0,0 +1,209 @@ +--TEST-- +array_map(): foreach optimization - function argument is evaluated once +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.opt_debug_level=0x20000 +--FILE-- + +--EXPECTF-- +$_main: + ; (lines=%d, args=0, vars=%d, tmps=%d) + ; (after optimizer) + ; %s +0000 INIT_FCALL 2 %d string("range") +0001 SEND_VAL int(1) 1 +0002 SEND_VAL int(2) 2 +0003 T2 = DO_ICALL +0004 ASSIGN CV0($array) T2 +0005 INIT_FCALL 1 %d string("var_dump") +0006 INIT_FCALL 0 %d string("get_function") +0007 T3 = DO_UCALL +0008 TYPE_ASSERT 131079 string("array_map") CV0($array) +0009 T2 = INIT_ARRAY 0 (packed) NEXT +0010 V4 = FE_RESET_R CV0($array) 0018 +0011 T8 = FE_FETCH_R V4 T7 0018 +0012 T9 = COPY_TMP T3 +0013 INIT_DYNAMIC_CALL 1 T9 +0014 SEND_VAL_EX T7 1 +0015 T7 = DO_FCALL +0016 T2 = ADD_ARRAY_ELEMENT T7 T8 +0017 JMP 0011 +0018 FE_FREE V4 +0019 FREE T3 +0020 SEND_VAL T2 1 +0021 DO_ICALL +0022 INIT_FCALL 1 %d string("var_dump") +0023 T2 = NEW 0 string("C") +0024 DO_FCALL +0025 T3 = QM_ASSIGN T2 +0026 TYPE_ASSERT 131079 string("array_map") CV0($array) +0027 T2 = INIT_ARRAY 0 (packed) NEXT +0028 V4 = FE_RESET_R CV0($array) 0037 +0029 T7 = FE_FETCH_R V4 T6 0037 +0030 T9 = COPY_TMP T3 +0031 V8 = FETCH_CLASS (exception) T9 +0032 INIT_STATIC_METHOD_CALL 1 V8 string("f") +0033 SEND_VAL_EX T6 1 +0034 T6 = DO_FCALL +0035 T2 = ADD_ARRAY_ELEMENT T6 T7 +0036 JMP 0029 +0037 FE_FREE V4 +0038 FREE T3 +0039 SEND_VAL T2 1 +0040 DO_ICALL +0041 T2 = DECLARE_LAMBDA_FUNCTION 4294967295 0 +0042 BIND_LEXICAL (ref) T2 CV1($f) +0043 ASSIGN CV1($f) T2 +0044 INIT_FCALL 1 %d string("var_dump") +0045 T3 = QM_ASSIGN CV1($f) +0046 TYPE_ASSERT 131079 string("array_map") CV0($array) +0047 T2 = INIT_ARRAY 0 (packed) NEXT +0048 V4 = FE_RESET_R CV0($array) 0056 +0049 T6 = FE_FETCH_R V4 T5 0056 +0050 T7 = COPY_TMP T3 +0051 INIT_DYNAMIC_CALL 1 T7 +0052 SEND_VAL_EX T5 1 +0053 T5 = DO_FCALL +0054 T2 = ADD_ARRAY_ELEMENT T5 T6 +0055 JMP 0049 +0056 FE_FREE V4 +0057 FREE T3 +0058 SEND_VAL T2 1 +0059 DO_ICALL +0060 RETURN int(1) +LIVE RANGES: + 3: 0008 - 0019 (tmp/var) + 2: 0010 - 0020 (tmp/var) + 4: 0011 - 0018 (loop) + 7: 0012 - 0014 (tmp/var) + 8: 0012 - 0016 (tmp/var) + 2: 0024 - 0025 (new) + 3: 0026 - 0038 (tmp/var) + 2: 0028 - 0039 (tmp/var) + 4: 0029 - 0037 (loop) + 6: 0030 - 0033 (tmp/var) + 7: 0030 - 0035 (tmp/var) + 2: 0042 - 0043 (tmp/var) + 3: 0046 - 0057 (tmp/var) + 2: 0048 - 0058 (tmp/var) + 4: 0049 - 0056 (loop) + 5: 0050 - 0052 (tmp/var) + 6: 0050 - 0054 (tmp/var) + +{closure:%s:%d}: + ; (lines=5, args=1, vars=2, tmps=%d) + ; (after optimizer) + ; %s.php:%s +0000 CV0($value) = RECV 1 +0001 BIND_STATIC (ref) CV1($f) +0002 ASSIGN CV1($f) string("dechex") +0003 T2 = ADD CV0($value) int(1) +0004 RETURN T2 + +plusn: + ; (lines=4, args=2, vars=2, tmps=%d) + ; (after optimizer) + ; %s +0000 CV0($x) = RECV 1 +0001 CV1($n) = RECV 2 +0002 T2 = ADD CV0($x) CV1($n) +0003 RETURN T2 + +get_function: + ; (lines=7, args=0, vars=0, tmps=%d) + ; (after optimizer) + ; %s.php:%s +0000 INIT_FCALL 1 %d string("var_dump") +0001 SEND_VAL string("get_function") 1 +0002 DO_ICALL +0003 V1 = DECLARE_ANON_CLASS string("class@anonymous%s") +0004 T0 = NEW 0 V1 +0005 DO_FCALL +0006 RETURN T0 +LIVE RANGES: + 0: 0005 - 0006 (new) + +class@anonymous::__invoke: + ; (lines=3, args=1, vars=1, tmps=%d) + ; (after optimizer) + ; %s.php:%s +0000 CV0($value) = RECV 1 +0001 T1 = ADD CV0($value) int(1) +0002 RETURN T1 + +C::__construct: + ; (lines=4, args=0, vars=0, tmps=%d) + ; (after optimizer) + ; %s.php:%s +0000 INIT_FCALL 1 %d string("var_dump") +0001 SEND_VAL string("C::__construct") 1 +0002 DO_ICALL +0003 RETURN null + +C::f: + ; (lines=3, args=1, vars=1, tmps=%d) + ; (after optimizer) + ; %s.php:%s +0000 CV0($value) = RECV 1 +0001 T1 = ADD CV0($value) int(1) +0002 RETURN T1 +string(12) "get_function" +array(2) { + [0]=> + int(2) + [1]=> + int(3) +} +string(14) "C::__construct" +array(2) { + [0]=> + int(2) + [1]=> + int(3) +} +array(2) { + [0]=> + int(2) + [1]=> + int(3) +} diff --git a/ext/opcache/tests/array_map_foreach_optimization_012.phpt b/ext/opcache/tests/array_map_foreach_optimization_012.phpt new file mode 100644 index 000000000000..82336f69f2aa --- /dev/null +++ b/ext/opcache/tests/array_map_foreach_optimization_012.phpt @@ -0,0 +1,174 @@ +--TEST-- +array_map(): foreach optimization - PFA with non-literal args are not optimizable +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.opt_debug_level=0x20000 +--ENV-- +A=1 +--FILE-- +value), range(1, 2))); + var_dump(array_map(timesn(?, $obj->value), range(1, 2))); + var_dump($obj); +} + +f((object) ['value' => 1]); + +?> +--EXPECTF-- +$_main: + ; (lines=%d, args=0, vars=%d, tmps=%d) + ; (after optimizer) + ; %s +0000 INIT_FCALL 1 %d string("getenv") +0001 SEND_VAL string("A") 1 +0002 T0 = DO_ICALL +0003 JMPZ T0 0006 +0004 DECLARE_FUNCTION string("plusn") 0 +0005 DECLARE_FUNCTION string("timesn") 1 +0006 INIT_FCALL 1 %d string("f") +0007 T0 = CAST (object) array(...) +0008 SEND_VAL T0 1 +0009 DO_UCALL +0010 RETURN int(1) + +plusn: + ; (lines=4, args=2, vars=2, tmps=%d) + ; (after optimizer) + ; %s +0000 CV0($x) = RECV 1 +0001 CV1($n) = RECV 2 +0002 T2 = ADD CV0($x) CV1($n) +0003 RETURN T2 + +timesn: + ; (lines=5, args=2, vars=3, tmps=%d) + ; (after optimizer) + ; %s.php:%s +0000 CV0($x) = RECV 1 +0001 CV1($n) = RECV 2 +0002 CV2($result) = MUL CV0($x) CV1($n) +0003 ASSIGN CV1($n) int(0) +0004 RETURN CV2($result) + +f: + ; (lines=39, args=1, vars=1, tmps=%d) + ; (after optimizer) + ; %s.php:%s +0000 CV0($obj) = RECV 1 +0001 INIT_FCALL 1 %d string("var_dump") +0002 INIT_FCALL 2 %d string("array_map") +0003 INIT_FCALL_BY_NAME 2 string("plusn") +0004 SEND_PLACEHOLDER 1 +0005 CHECK_FUNC_ARG 2 +0006 V1 = FETCH_OBJ_FUNC_ARG (ref) CV0($obj) string("value") +0007 SEND_FUNC_ARG V1 2 +0008 T1 = CALLABLE_CONVERT_PARTIAL %d string("{closure:pfa:f():15}") +0009 SEND_VAL T1 1 +0010 INIT_FCALL 2 %d string("range") +0011 SEND_VAL int(1) 1 +0012 SEND_VAL int(2) 2 +0013 T1 = DO_ICALL +0014 SEND_VAL T1 2 +0015 T1 = DO_ICALL +0016 SEND_VAL T1 1 +0017 DO_ICALL +0018 INIT_FCALL 1 %d string("var_dump") +0019 INIT_FCALL 2 %d string("array_map") +0020 INIT_FCALL_BY_NAME 2 string("timesn") +0021 SEND_PLACEHOLDER 1 +0022 CHECK_FUNC_ARG 2 +0023 V1 = FETCH_OBJ_FUNC_ARG (ref) CV0($obj) string("value") +0024 SEND_FUNC_ARG V1 2 +0025 T1 = CALLABLE_CONVERT_PARTIAL %d string("{closure:pfa:f():16}") +0026 SEND_VAL T1 1 +0027 INIT_FCALL 2 %d string("range") +0028 SEND_VAL int(1) 1 +0029 SEND_VAL int(2) 2 +0030 T1 = DO_ICALL +0031 SEND_VAL T1 2 +0032 T1 = DO_ICALL +0033 SEND_VAL T1 1 +0034 DO_ICALL +0035 INIT_FCALL 1 %d string("var_dump") +0036 SEND_VAR CV0($obj) 1 +0037 DO_ICALL +0038 RETURN null + +$_main: + ; (lines=4, args=0, vars=1, tmps=%d) + ; (after optimizer) + ; %s.php:%s +0000 T1 = DECLARE_LAMBDA_FUNCTION 4294967295 0 +0001 BIND_LEXICAL T1 CV0($n) +0002 FREE T1 +0003 RETURN int(1) +LIVE RANGES: + 1: 0001 - 0002 (tmp/var) + +{closure:pfa:f():15}: + ; (lines=7, args=1, vars=2, tmps=%d) + ; (after optimizer) + ; %s.php:%s +0000 CV0($x) = RECV 1 +0001 BIND_STATIC CV1($n) +0002 INIT_FCALL 2 %d string("plusn") +0003 SEND_VAR CV0($x) 1 +0004 SEND_VAR CV1($n) 2 +0005 T2 = DO_UCALL +0006 RETURN T2 +array(2) { + [0]=> + int(2) + [1]=> + int(3) +} + +$_main: + ; (lines=4, args=0, vars=1, tmps=%d) + ; (after optimizer) + ; %s.php:%s +0000 T1 = DECLARE_LAMBDA_FUNCTION 4294967295 0 +0001 BIND_LEXICAL (ref) T1 CV0($n) +0002 FREE T1 +0003 RETURN int(1) +LIVE RANGES: + 1: 0001 - 0002 (tmp/var) + +{closure:pfa:f():16}: + ; (lines=7, args=1, vars=2, tmps=%d) + ; (after optimizer) + ; %s.php:%s +0000 CV0($x) = RECV 1 +0001 BIND_STATIC (ref) CV1($n) +0002 INIT_FCALL 2 %d string("timesn") +0003 SEND_VAR CV0($x) 1 +0004 SEND_REF CV1($n) 2 +0005 T2 = DO_UCALL +0006 RETURN T2 +array(2) { + [0]=> + int(1) + [1]=> + int(0) +} +object(stdClass)#%d (1) { + ["value"]=> + int(0) +}