Commit 40a8468559a for php
commit 40a8468559ab5b1918acbe907b0e7ab951192590
Author: Arnaud Le Blanc <365207+arnaud-lb@users.noreply.github.com>
Date: Wed Sep 30 14:25:26 2026 +0200
Fix array_map optimization with non-literal function or non-literal args (#23254)
Non-literal expressions must be evaluated once and memoized to maintain
semantics.
diff --git a/NEWS b/NEWS
index 9a8bf446cf4..79307987384 100644
--- a/NEWS
+++ b/NEWS
@@ -32,6 +32,8 @@ PHP NEWS
constant in use declarations. (Girgias)
. Switch the POSIX high resolution timer back to CLOCK_MONOTONIC from
CLOCK_MONOTONIC_RAW. (nicolas-grekas)
+ . Fixed array_map optimization with non-literal function or non-literal args.
+ (Arnaud)
- FFI:
. Fixed crashes with FFI callbacks created from __call() trampolines
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index bad60a5f107..e9737dabedb 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -5335,11 +5335,64 @@ static zend_result zend_compile_func_array_map(znode *result, zend_ast_list *arg
return FAILURE;
}
+ /* Bail out if callback is not a first class callable */
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 || result.op_type == IS_TMP_VAR) {
+ 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 || result.op_type == IS_TMP_VAR) {
+ 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();
@@ -5348,6 +5401,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;
}
@@ -5387,14 +5446,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 */
@@ -5409,6 +5489,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 889fee27b8a..4c557ce6802 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 @@ function plus1($x) {
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_010.phpt b/ext/opcache/tests/array_map_foreach_optimization_010.phpt
new file mode 100644
index 00000000000..758c3504a88
--- /dev/null
+++ b/ext/opcache/tests/array_map_foreach_optimization_010.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--
+<?php
+
+function cand_86013_trusted(string $value): string
+{
+ return 'trusted:' . $value;
+}
+
+function cand_86013_unexpected(string $value): string
+{
+ return 'unexpected:' . $value;
+}
+
+function cand_86013_input_changes_target(): array
+{
+ global $callback;
+ global $obj;
+ $callback = 'cand_86013_unexpected';
+ $obj = new Unexpected;
+ return ['payload'];
+}
+
+class Trusted {
+ static function f($value) {
+ return 'trusted:' . $value;
+ }
+}
+
+class Unexpected {
+ static function f($value) {
+ return 'unexpected:' . $value;
+ }
+}
+
+$callback = 'cand_86013_trusted';
+echo "direct array_map\n";
+var_dump(array_map($callback(...), cand_86013_input_changes_target()));
+
+$callback = 'cand_86013_trusted';
+$array_map = 'array_map';
+echo "dynamic-call control\n";
+var_dump($array_map($callback(...), cand_86013_input_changes_target()));
+
+$missing = 'cand_86013_missing';
+echo "empty direct\n";
+try {
+ var_dump(array_map($missing(...), []));
+} catch (Throwable $e) {
+ echo get_class($e), ': ', $e->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_011.phpt b/ext/opcache/tests/array_map_foreach_optimization_011.phpt
new file mode 100644
index 00000000000..764eb22d9d6
--- /dev/null
+++ b/ext/opcache/tests/array_map_foreach_optimization_011.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--
+<?php
+
+function cand_86013_bound_value(): string
+{
+ global $bound_calls;
+ echo 'BOUND:', ++$bound_calls, "\n";
+ return 'b';
+}
+
+function cand_86013_input(): array
+{
+ echo "INPUT\n";
+ return ['a', 'a', 'a'];
+}
+
+$bound_calls = 0;
+echo "direct array_map\n";
+$direct = array_map(
+ str_replace('a', cand_86013_bound_value(), ?),
+ cand_86013_input(),
+);
+var_dump($direct, $bound_calls);
+
+$bound_calls = 0;
+$array_map = 'array_map';
+echo "dynamic-call control\n";
+$control = $array_map(
+ str_replace('a', cand_86013_bound_value(), ?),
+ cand_86013_input(),
+);
+var_dump($control, $bound_calls);
+
+?>
+--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_012.phpt b/ext/opcache/tests/array_map_foreach_optimization_012.phpt
new file mode 100644
index 00000000000..04b16d2ab6d
--- /dev/null
+++ b/ext/opcache/tests/array_map_foreach_optimization_012.phpt
@@ -0,0 +1,208 @@
+--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--
+<?php
+
+function plusn($x, $n) {
+ return $x + $n;
+}
+
+function get_function() {
+ var_dump(__FUNCTION__);
+ return new class {
+ function __invoke($value) {
+ return $value + 1;
+ }
+ };
+}
+
+class C {
+ function __construct() {
+ var_dump(__METHOD__);
+ }
+ static function f($value) {
+ return $value + 1;
+ }
+}
+
+$array = range(1, 2);
+
+var_dump(array_map(get_function()(...), $array));
+
+var_dump(array_map((new C)::f(...), $array));
+
+$f = function ($value) use (&$f) {
+ $f = 'dechex';
+ return $value + 1;
+};
+
+var_dump(array_map($f(...), $array));
+
+?>
+--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 T3 = NEW 0 string("C")
+0024 DO_FCALL
+0025 TYPE_ASSERT 131079 string("array_map") CV0($array)
+0026 T2 = INIT_ARRAY 0 (packed) NEXT
+0027 V4 = FE_RESET_R CV0($array) 0036
+0028 T7 = FE_FETCH_R V4 T6 0036
+0029 T9 = COPY_TMP T3
+0030 V8 = FETCH_CLASS (exception) T9
+0031 INIT_STATIC_METHOD_CALL 1 V8 string("f")
+0032 SEND_VAL_EX T6 1
+0033 T6 = DO_FCALL
+0034 T2 = ADD_ARRAY_ELEMENT T6 T7
+0035 JMP 0028
+0036 FE_FREE V4
+0037 FREE T3
+0038 SEND_VAL T2 1
+0039 DO_ICALL
+0040 T2 = DECLARE_LAMBDA_FUNCTION 4294967295 0
+0041 BIND_LEXICAL (ref) T2 CV1($f)
+0042 ASSIGN CV1($f) T2
+0043 INIT_FCALL 1 %d string("var_dump")
+0044 T3 = QM_ASSIGN CV1($f)
+0045 TYPE_ASSERT 131079 string("array_map") CV0($array)
+0046 T2 = INIT_ARRAY 0 (packed) NEXT
+0047 V4 = FE_RESET_R CV0($array) 0055
+0048 T6 = FE_FETCH_R V4 T5 0055
+0049 T7 = COPY_TMP T3
+0050 INIT_DYNAMIC_CALL 1 T7
+0051 SEND_VAL_EX T5 1
+0052 T5 = DO_FCALL
+0053 T2 = ADD_ARRAY_ELEMENT T5 T6
+0054 JMP 0048
+0055 FE_FREE V4
+0056 FREE T3
+0057 SEND_VAL T2 1
+0058 DO_ICALL
+0059 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)
+ 3: 0024 - 0025 (new)
+ 3: 0025 - 0037 (tmp/var)
+ 2: 0027 - 0038 (tmp/var)
+ 4: 0028 - 0036 (loop)
+ 6: 0029 - 0032 (tmp/var)
+ 7: 0029 - 0034 (tmp/var)
+ 2: 0041 - 0042 (tmp/var)
+ 3: 0045 - 0056 (tmp/var)
+ 2: 0047 - 0057 (tmp/var)
+ 4: 0048 - 0055 (loop)
+ 5: 0049 - 0051 (tmp/var)
+ 6: 0049 - 0053 (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_013.phpt b/ext/opcache/tests/array_map_foreach_optimization_013.phpt
new file mode 100644
index 00000000000..e831fc3e2b3
--- /dev/null
+++ b/ext/opcache/tests/array_map_foreach_optimization_013.phpt
@@ -0,0 +1,174 @@
+--TEST--
+array_map(): foreach optimization - PFAs 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--
+<?php
+
+if (getenv('A')) {
+ function plusn($x, $n) {
+ return $x + $n;
+ }
+ function timesn($x, &$n) {
+ $result = $x * $n;
+ $n = 0;
+ return $result;
+ }
+}
+
+function f($obj) {
+ var_dump(array_map(plusn(?, $obj->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)
+}
diff --git a/ext/opcache/tests/array_map_foreach_optimization_014.phpt b/ext/opcache/tests/array_map_foreach_optimization_014.phpt
new file mode 100644
index 00000000000..c6c4afc216a
--- /dev/null
+++ b/ext/opcache/tests/array_map_foreach_optimization_014.phpt
@@ -0,0 +1,70 @@
+--TEST--
+array_map(): foreach optimization - PFAs with literal args are optimizable
+--EXTENSIONS--
+opcache
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.opt_debug_level=0x20000
+--ENV--
+A=1
+--FILE--
+<?php
+
+if (getenv('A')) {
+ function plusn($x, $n) {
+ return $x + $n;
+ }
+}
+
+var_dump(array_map(plusn(?, 2), range(1, 2)));
+
+?>
+--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 0005
+0004 DECLARE_FUNCTION string("plusn") 0
+0005 INIT_FCALL 1 %d string("var_dump")
+0006 INIT_FCALL 2 %d string("range")
+0007 SEND_VAL int(1) 1
+0008 SEND_VAL int(2) 2
+0009 T3 = DO_ICALL
+0010 TYPE_ASSERT 131079 string("array_map") T3
+0011 T0 = INIT_ARRAY 0 (packed) NEXT
+0012 V1 = FE_RESET_R T3 0020
+0013 T3 = FE_FETCH_R V1 T2 0020
+0014 INIT_FCALL_BY_NAME 2 string("plusn")
+0015 SEND_VAL_EX T2 1
+0016 SEND_VAL_EX int(2) 2
+0017 T2 = DO_FCALL_BY_NAME
+0018 T0 = ADD_ARRAY_ELEMENT T2 T3
+0019 JMP 0013
+0020 FE_FREE V1
+0021 SEND_VAL T0 1
+0022 DO_ICALL
+0023 RETURN int(1)
+LIVE RANGES:
+ 3: 0010 - 0012 (tmp/var)
+ 0: 0012 - 0021 (tmp/var)
+ 1: 0013 - 0020 (loop)
+
+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
+array(2) {
+ [0]=>
+ int(3)
+ [1]=>
+ int(4)
+}