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(&copy_node, ZEND_COPY_TMP, &func_node, NULL);
+		func_ast = zend_ast_create_znode(&copy_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(&copy_node, ZEND_COPY_TMP, &class_node, NULL);
+				class_ast = zend_ast_create_znode(&copy_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)
+}