Commit a3576bddc52 for php.net

commit a3576bddc52310e8ed6c545e38104e884713ceef
Author: Tim Düsterhus <tim@tideways-gmbh.com>
Date:   Mon Jan 19 10:18:24 2026 +0100

    zend_compile: Optimize `array_map()` with callable convert callback into foreach (#20934)

    * zend_compile: Optimize `array_map()` with callable convert callback into foreach

    For:

        <?php

        function plus1($x) {
            return $x + 1;
        }

        $array = array_fill(0, 100, 1);

        $count = 0;
        for ($i = 0; $i < 100_000; $i++) {
            $count += count(array_map(plus1(...), $array));
        }

        var_dump($count);

    This is ~1.1× faster:

        Benchmark 1: /tmp/test/before -d opcache.enable_cli=1 /tmp/test/test6.php
          Time (mean ± σ):     172.2 ms ±   0.5 ms    [User: 167.8 ms, System: 4.2 ms]
          Range (min … max):   171.6 ms … 173.1 ms    17 runs

        Benchmark 2: /tmp/test/after -d opcache.enable_cli=1 /tmp/test/test6.php
          Time (mean ± σ):     155.1 ms ±   1.3 ms    [User: 150.6 ms, System: 4.2 ms]
          Range (min … max):   154.2 ms … 159.3 ms    18 runs

        Summary
          /tmp/test/after -d opcache.enable_cli=1 /tmp/test/test6.php ran
            1.11 ± 0.01 times faster than /tmp/test/before -d opcache.enable_cli=1 /tmp/test/test6.php

    With JIT it becomes ~1.7× faster:

        Benchmark 1: /tmp/test/before -d opcache.enable_cli=1 -d opcache.jit=tracing /tmp/test/test6.php
          Time (mean ± σ):     166.9 ms ±   0.6 ms    [User: 162.7 ms, System: 4.1 ms]
          Range (min … max):   166.1 ms … 167.9 ms    17 runs

        Benchmark 2: /tmp/test/after -d opcache.enable_cli=1 -d opcache.jit=tracing /tmp/test/test6.php
          Time (mean ± σ):      94.5 ms ±   2.7 ms    [User: 90.4 ms, System: 3.9 ms]
          Range (min … max):    92.5 ms … 103.1 ms    31 runs

        Summary
          /tmp/test/after -d opcache.enable_cli=1 -d opcache.jit=tracing /tmp/test/test6.php ran
            1.77 ± 0.05 times faster than /tmp/test/before -d opcache.enable_cli=1 -d opcache.jit=tracing /tmp/test/test6.php

    * zend_compile: Skip `assert(...)` callbacks for array_map() optimization

    * zend_compile: Remove `zend_eval_const_expr()` in array_map optimization

    * zend_vm_def: Check simple types without loading the arginfo in ZEND_TYPE_ASSERT

    * zend_vm_def: Handle references for ZEND_TYPE_ASSERT

    * zend_compile: Fix handling of constant arrays for `array_map()`

    * zend_compile: Fix leak of unused result in array_map() optimization

    * zend_compile: Support static methods for `array_map()` optimization

    * UPGRADING

diff --git a/UPGRADING b/UPGRADING
index 0b651e5895c..3d0a56756cd 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -151,6 +151,11 @@ PHP 8.6 UPGRADE NOTES
     parsing the format string.
   . Arguments are now passed more efficiently to known constructors (e.g. when
     using new self()).
+  . array_map() using a first-class callable or partial function application
+    callback will be compiled into the equivalent foreach-loop, avoiding the
+    creation of intermediate Closures, the overhead of calling userland
+    callbacks from internal functions and providing for better insight for the
+    JIT.

 - DOM:
   . Made splitText() faster and consume less memory.
diff --git a/Zend/tests/functions/zend_call_function_deprecated_frame.phpt b/Zend/tests/functions/zend_call_function_deprecated_frame.phpt
index ce2943a876e..83d05144c0b 100644
--- a/Zend/tests/functions/zend_call_function_deprecated_frame.phpt
+++ b/Zend/tests/functions/zend_call_function_deprecated_frame.phpt
@@ -19,7 +19,6 @@ function foo(string $v) {
 --EXPECTF--
 Fatal error: Uncaught Exception: Function foo() is deprecated in %s:%d
 Stack trace:
-#0 [internal function]: {closure:%s:%d}(16384, 'Function foo() ...', '%s', %d)
-#1 %s(%d): array_map(Object(Closure), Array)
-#2 {main}
+#0 %s(%d): {closure:%s:%d}(16384, 'Function foo() ...', '%s', %d)
+#1 {main}
   thrown in %s on line %d
diff --git a/Zend/tests/gh14003.phpt b/Zend/tests/gh14003.phpt
index 92a6c5919ab..8d25cad863c 100644
--- a/Zend/tests/gh14003.phpt
+++ b/Zend/tests/gh14003.phpt
@@ -18,7 +18,6 @@ function foo(string $key): string {
 --EXPECTF--
 Fatal error: Uncaught Exception: Test in %s:%d
 Stack trace:
-#0 [internal function]: foo('a')
-#1 %s(%d): array_map(Object(Closure), Array)
-#2 {main}
+#0 %s(%d): foo('a')
+#1 {main}
   thrown in %s on line %d
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index 203201bcc12..916d8eebd89 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -5027,7 +5027,115 @@ static zend_result zend_compile_func_clone(znode *result, const zend_ast_list *a
 	return SUCCESS;
 }

-static zend_result zend_try_compile_special_func_ex(znode *result, zend_string *lcname, zend_ast_list *args, uint32_t type) /* {{{ */
+static zend_result zend_compile_func_array_map(znode *result, zend_ast_list *args, zend_string *lcname, uint32_t lineno) /* {{{ */
+{
+	/* Bail out if we do not have exactly two parameters. */
+	if (args->children != 2) {
+		return FAILURE;
+	}
+
+	zend_ast *callback = args->child[0];
+
+	/* Bail out if the callback is not a FCC/PFA. */
+	zend_ast *args_ast;
+	switch (callback->kind) {
+		case ZEND_AST_CALL:
+		case ZEND_AST_STATIC_CALL:
+			args_ast = zend_ast_call_get_args(callback);
+			if (args_ast->kind != ZEND_AST_CALLABLE_CONVERT) {
+				return FAILURE;
+			}
+
+			break;
+		default:
+			return FAILURE;
+	}
+
+	/* Bail out if the callback is assert() due to the AST stringification logic
+	 * breaking for the generated call.
+	 */
+	if (callback->kind == ZEND_AST_CALL && zend_string_equals_literal_ci(zend_ast_get_str(callback->child[0]), "assert")) {
+		return FAILURE;
+	}
+
+	znode value;
+	value.op_type = IS_TMP_VAR;
+	value.u.op.var = get_temporary_variable();
+
+	zend_ast_list *callback_args = zend_ast_get_list(((zend_ast_fcc*)args_ast)->args);
+	zend_ast *call_args = zend_ast_create_list(0, ZEND_AST_ARG_LIST);
+	for (uint32_t i = 0; i < callback_args->children; i++) {
+		zend_ast *child = callback_args->child[i];
+		if (child->kind == ZEND_AST_PLACEHOLDER_ARG) {
+			call_args = zend_ast_list_add(call_args, zend_ast_create_znode(&value));
+		} else {
+			ZEND_ASSERT(0 && "not implemented");
+			call_args = zend_ast_list_add(call_args, child);
+		}
+	}
+
+	zend_op *opline;
+
+	znode array;
+	zend_compile_expr(&array, args->child[1]);
+	/* array is an argument to both ZEND_TYPE_ASSERT and to ZEND_FE_RESET_R. */
+	if (array.op_type == IS_CONST) {
+		Z_TRY_ADDREF(array.u.constant);
+	}
+
+	/* Verify that the input array actually is an array. */
+	znode name;
+	name.op_type = IS_CONST;
+	ZVAL_STR_COPY(&name.u.constant, lcname);
+	opline = zend_emit_op(NULL, ZEND_TYPE_ASSERT, &name, &array);
+	opline->lineno = lineno;
+	opline->extended_value = (2 << 16) | IS_ARRAY;
+	const zval *fbc_zv = zend_hash_find(CG(function_table), lcname);
+	const Bucket *fbc_bucket = (const Bucket*)((uintptr_t)fbc_zv - XtOffsetOf(Bucket, val));
+	Z_EXTRA_P(CT_CONSTANT(opline->op1)) = fbc_bucket - CG(function_table)->arData;
+
+	/* Initialize the result array. */
+	zend_emit_op_tmp(result, ZEND_INIT_ARRAY, NULL, NULL);
+
+	/* foreach loop starts here. */
+	znode key;
+
+	uint32_t opnum_reset = get_next_op_number();
+	znode reset_node;
+	zend_emit_op(&reset_node, ZEND_FE_RESET_R, &array, NULL);
+	zend_begin_loop(ZEND_FE_FREE, &reset_node, false);
+	uint32_t opnum_fetch = get_next_op_number();
+	zend_emit_op_tmp(&key, ZEND_FE_FETCH_R, &reset_node, &value);
+
+	/* loop body */
+	znode call_result;
+	switch (callback->kind) {
+		case ZEND_AST_CALL:
+			zend_compile_expr(&call_result, zend_ast_create(ZEND_AST_CALL, callback->child[0], 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));
+			break;
+	}
+	opline = zend_emit_op(NULL, ZEND_ADD_ARRAY_ELEMENT, &call_result, &key);
+	SET_NODE(opline->result, result);
+	/* end loop body */
+
+	zend_emit_jump(opnum_fetch);
+
+	uint32_t opnum_loop_end = get_next_op_number();
+	opline = &CG(active_op_array)->opcodes[opnum_reset];
+	opline->op2.opline_num = opnum_loop_end;
+	opline = &CG(active_op_array)->opcodes[opnum_fetch];
+	opline->extended_value = opnum_loop_end;
+
+	zend_end_loop(opnum_fetch, &reset_node);
+	zend_emit_op(NULL, ZEND_FE_FREE, &reset_node, NULL);
+
+	return SUCCESS;
+}
+
+static zend_result zend_try_compile_special_func_ex(znode *result, zend_string *lcname, zend_ast_list *args, uint32_t type, uint32_t lineno) /* {{{ */
 {
 	if (zend_string_equals_literal(lcname, "strlen")) {
 		return zend_compile_func_strlen(result, args);
@@ -5099,12 +5207,14 @@ static zend_result zend_try_compile_special_func_ex(znode *result, zend_string *
 		return zend_compile_func_printf(result, args);
 	} else if (zend_string_equals(lcname, ZSTR_KNOWN(ZEND_STR_CLONE))) {
 		return zend_compile_func_clone(result, args);
+	} else if (zend_string_equals_literal(lcname, "array_map")) {
+		return zend_compile_func_array_map(result, args, lcname, lineno);
 	} else {
 		return FAILURE;
 	}
 }

-static zend_result zend_try_compile_special_func(znode *result, zend_string *lcname, zend_ast_list *args, const zend_function *fbc, uint32_t type) /* {{{ */
+static zend_result zend_try_compile_special_func(znode *result, zend_string *lcname, zend_ast_list *args, const zend_function *fbc, uint32_t type, uint32_t lineno) /* {{{ */
 {
 	if (CG(compiler_options) & ZEND_COMPILE_NO_BUILTINS) {
 		return FAILURE;
@@ -5120,7 +5230,7 @@ static zend_result zend_try_compile_special_func(znode *result, zend_string *lcn
 		return FAILURE;
 	}

-	if (zend_try_compile_special_func_ex(result, lcname, args, type) == SUCCESS) {
+	if (zend_try_compile_special_func_ex(result, lcname, args, type, lineno) == SUCCESS) {
 		return SUCCESS;
 	}

@@ -5263,7 +5373,7 @@ static void zend_compile_call(znode *result, const zend_ast *ast, uint32_t type)

 		if (!is_callable_convert &&
 		    zend_try_compile_special_func(result, lcname,
-				zend_ast_get_list(args_ast), fbc, type) == SUCCESS
+				zend_ast_get_list(args_ast), fbc, type, ast->lineno) == SUCCESS
 		) {
 			zend_string_release_ex(lcname, 0);
 			zval_ptr_dtor(&name_node.u.constant);
diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h
index 9840bf28040..2d059159a23 100644
--- a/Zend/zend_vm_def.h
+++ b/Zend/zend_vm_def.h
@@ -8846,6 +8846,38 @@ ZEND_VM_C_LABEL(type_check_resource):
 	}
 }

+ZEND_VM_HOT_HANDLER(211, ZEND_TYPE_ASSERT, CONST, ANY, NUM)
+{
+	USE_OPLINE
+	SAVE_OPLINE();
+
+	zval *value = GET_OP2_ZVAL_PTR_UNDEF(BP_VAR_R);
+
+	uint8_t actual_type = Z_TYPE_P(value);
+	uint8_t expected_type = opline->extended_value & 0xff;
+	/* Simple types can be checked directly. */
+	if (UNEXPECTED(actual_type != expected_type)) {
+		zend_function *fbc;
+		{
+			zval *fname = (zval*)RT_CONSTANT(opline, opline->op1);
+			ZEND_ASSERT(Z_EXTRA_P(fname) != 0);
+			fbc = Z_FUNC(EG(function_table)->arData[Z_EXTRA_P(fname)].val);
+			ZEND_ASSERT(fbc->type != ZEND_USER_FUNCTION);
+		}
+		uint16_t argno = opline->extended_value >> 16;
+		zend_arg_info *arginfo = &fbc->common.arg_info[argno - 1];
+
+		if (!zend_check_type(&arginfo->type, value, /* is_return_type */ false, /* is_internal */ true)) {
+			const char *param_name = get_function_arg_name(fbc, argno);
+			zend_string *expected = zend_type_to_string(arginfo->type);
+			zend_type_error("%s(): Argument #%d%s%s%s must be of type %s, %s given", ZSTR_VAL(fbc->common.function_name), argno, param_name ? " ($" : "", param_name ? param_name : "", param_name ? ")" : "", ZSTR_VAL(expected), zend_zval_value_name(value));
+			zend_string_release(expected);
+		}
+	}
+
+	ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
+}
+
 ZEND_VM_HOT_HANDLER(122, ZEND_DEFINED, CONST, ANY, CACHE_SLOT)
 {
 	USE_OPLINE
diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h
index 7aa0296aa42..38b869c4a4c 100644
Binary files a/Zend/zend_vm_execute.h and b/Zend/zend_vm_execute.h differ
diff --git a/Zend/zend_vm_handlers.h b/Zend/zend_vm_handlers.h
index 33d95114155..d52de1226df 100644
Binary files a/Zend/zend_vm_handlers.h and b/Zend/zend_vm_handlers.h differ
diff --git a/Zend/zend_vm_opcodes.c b/Zend/zend_vm_opcodes.c
index 1c7a31b3861..cb70e8ccd86 100644
Binary files a/Zend/zend_vm_opcodes.c and b/Zend/zend_vm_opcodes.c differ
diff --git a/Zend/zend_vm_opcodes.h b/Zend/zend_vm_opcodes.h
index dae282705d5..7aae4d0e55f 100644
Binary files a/Zend/zend_vm_opcodes.h and b/Zend/zend_vm_opcodes.h differ
diff --git a/ext/standard/tests/array/array_map_foreach_optimization_001.phpt b/ext/standard/tests/array/array_map_foreach_optimization_001.phpt
new file mode 100644
index 00000000000..49031942d20
--- /dev/null
+++ b/ext/standard/tests/array/array_map_foreach_optimization_001.phpt
@@ -0,0 +1,82 @@
+--TEST--
+array_map(): foreach optimization
+--EXTENSIONS--
+opcache
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.opt_debug_level=0x20000
+--FILE--
+<?php
+
+function plus1($x) {
+    return $x + 1;
+}
+
+$array = range(1, 10);
+
+$foo = array_map(plus1(...), $array);
+
+var_dump($foo);
+
+?>
+--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(10) 2
+0003 V2 = DO_ICALL
+0004 ASSIGN CV0($array) V2
+0005 TYPE_ASSERT 131079 string("array_map") CV0($array)
+0006 T2 = INIT_ARRAY 0 (packed) NEXT
+0007 V3 = FE_RESET_R CV0($array) 0014
+0008 T5 = FE_FETCH_R V3 T4 0014
+0009 INIT_FCALL 1 %d string("plus1")
+0010 SEND_VAL T4 1
+0011 V4 = DO_UCALL
+0012 T2 = ADD_ARRAY_ELEMENT V4 T5
+0013 JMP 0008
+0014 FE_FREE V3
+0015 ASSIGN CV1($foo) T2
+0016 INIT_FCALL 1 %d string("var_dump")
+0017 SEND_VAR CV1($foo) 1
+0018 DO_ICALL
+0019 RETURN int(1)
+LIVE RANGES:
+     2: 0007 - 0015 (tmp/var)
+     3: 0008 - 0014 (loop)
+     4: 0009 - 0010 (tmp/var)
+     5: 0009 - 0012 (tmp/var)
+
+plus1:
+     ; (lines=3, args=1, vars=1, tmps=1)
+     ; (after optimizer)
+     ; %s
+0000 CV0($x) = RECV 1
+0001 T1 = ADD CV0($x) int(1)
+0002 RETURN T1
+array(10) {
+  [0]=>
+  int(2)
+  [1]=>
+  int(3)
+  [2]=>
+  int(4)
+  [3]=>
+  int(5)
+  [4]=>
+  int(6)
+  [5]=>
+  int(7)
+  [6]=>
+  int(8)
+  [7]=>
+  int(9)
+  [8]=>
+  int(10)
+  [9]=>
+  int(11)
+}
diff --git a/ext/standard/tests/array/array_map_foreach_optimization_002.phpt b/ext/standard/tests/array/array_map_foreach_optimization_002.phpt
new file mode 100644
index 00000000000..79b7df08b61
--- /dev/null
+++ b/ext/standard/tests/array/array_map_foreach_optimization_002.phpt
@@ -0,0 +1,73 @@
+--TEST--
+array_map(): foreach optimization - Error
+--EXTENSIONS--
+opcache
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.opt_debug_level=0x20000
+--FILE--
+<?php
+
+function plus1($x) {
+    return $x + 1;
+}
+
+$array = \range(1, 10);
+
+$foo = array_map(
+     plus1(...),
+     new \ArrayIterator($array)
+);
+
+\var_dump($foo);
+
+?>
+--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(10) 2
+0003 V2 = DO_ICALL
+0004 ASSIGN CV0($array) V2
+0005 V5 = NEW 1 string("ArrayIterator")
+0006 SEND_VAR%S CV0($array) 1
+0007 DO_FCALL
+0008 TYPE_ASSERT 131079 string("array_map") V5
+0009 T2 = INIT_ARRAY 0 (packed) NEXT
+0010 V3 = FE_RESET_R V5 0017
+0011 T5 = FE_FETCH_R V3 T4 0017
+0012 INIT_FCALL 1 %d string("plus1")
+0013 SEND_VAL T4 1
+0014 V4 = DO_UCALL
+0015 T2 = ADD_ARRAY_ELEMENT V4 T5
+0016 JMP 0011
+0017 FE_FREE V3
+0018 ASSIGN CV1($foo) T2
+0019 INIT_FCALL 1 %d string("var_dump")
+0020 SEND_VAR CV1($foo) 1
+0021 DO_ICALL
+0022 RETURN int(1)
+LIVE RANGES:
+     5: 0006 - 0008 (new)
+     5: 0008 - 0010 (tmp/var)
+     2: 0010 - 0018 (tmp/var)
+     3: 0011 - 0017 (loop)
+     4: 0012 - 0013 (tmp/var)
+     5: 0012 - 0015 (tmp/var)
+
+plus1:
+     ; (lines=3, args=1, vars=1, tmps=1)
+     ; (after optimizer)
+     ; %s
+0000 CV0($x) = RECV 1
+0001 T1 = ADD CV0($x) int(1)
+0002 RETURN T1
+
+Fatal error: Uncaught TypeError: array_map(): Argument #2 ($array) must be of type array, ArrayIterator given in %s:9
+Stack trace:
+#0 {main}
+  thrown in %s on line 9
diff --git a/ext/standard/tests/array/array_map_foreach_optimization_003.phpt b/ext/standard/tests/array/array_map_foreach_optimization_003.phpt
new file mode 100644
index 00000000000..7a287f54f78
--- /dev/null
+++ b/ext/standard/tests/array/array_map_foreach_optimization_003.phpt
@@ -0,0 +1,73 @@
+--TEST--
+array_map(): foreach optimization - const array
+--EXTENSIONS--
+opcache
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.opt_debug_level=0x20000
+--FILE--
+<?php
+
+function plus1($x) {
+    return $x + 1;
+}
+
+$foo = array_map(plus1(...), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
+
+var_dump($foo);
+
+?>
+--EXPECTF--
+$_main:
+     ; (lines=%d, args=0, vars=%d, tmps=%d)
+     ; (after optimizer)
+     ; %s
+0000 TYPE_ASSERT 131079 string("array_map") array(...)
+0001 T1 = INIT_ARRAY 0 (packed) NEXT
+0002 V2 = FE_RESET_R array(...) 0009
+0003 T4 = FE_FETCH_R V2 T3 0009
+0004 INIT_FCALL 1 %d string("plus1")
+0005 SEND_VAL T3 1
+0006 V3 = DO_UCALL
+0007 T1 = ADD_ARRAY_ELEMENT V3 T4
+0008 JMP 0003
+0009 FE_FREE V2
+0010 ASSIGN CV0($foo) T1
+0011 INIT_FCALL 1 %d string("var_dump")
+0012 SEND_VAR CV0($foo) 1
+0013 DO_ICALL
+0014 RETURN int(1)
+LIVE RANGES:
+     1: 0002 - 0010 (tmp/var)
+     2: 0003 - 0009 (loop)
+
+plus1:
+     ; (lines=3, args=1, vars=1, tmps=1)
+     ; (after optimizer)
+     ; %s
+0000 CV0($x) = RECV 1
+0001 T1 = ADD CV0($x) int(1)
+0002 RETURN T1
+array(10) {
+  [0]=>
+  int(2)
+  [1]=>
+  int(3)
+  [2]=>
+  int(4)
+  [3]=>
+  int(5)
+  [4]=>
+  int(6)
+  [5]=>
+  int(7)
+  [6]=>
+  int(8)
+  [7]=>
+  int(9)
+  [8]=>
+  int(10)
+  [9]=>
+  int(11)
+}
diff --git a/ext/standard/tests/array/array_map_foreach_optimization_004.phpt b/ext/standard/tests/array/array_map_foreach_optimization_004.phpt
new file mode 100644
index 00000000000..b97ed1e31d3
--- /dev/null
+++ b/ext/standard/tests/array/array_map_foreach_optimization_004.phpt
@@ -0,0 +1,57 @@
+--TEST--
+array_map(): foreach optimization - unused refcounted result
+--EXTENSIONS--
+opcache
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.opt_debug_level=0x20000
+--FILE--
+<?php
+
+function stdClass() {
+    return new stdClass();
+}
+
+$array = range(1, 10);
+
+array_map(stdClass(...), $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(10) 2
+0003 V1 = DO_ICALL
+0004 ASSIGN CV0($array) V1
+0005 TYPE_ASSERT 131079 string("array_map") CV0($array)
+0006 T1 = INIT_ARRAY 0 (packed) NEXT
+0007 V2 = FE_RESET_R CV0($array) 0014
+0008 T4 = FE_FETCH_R V2 T3 0014
+0009 INIT_FCALL 1 %d string("stdclass")
+0010 SEND_VAL T3 1
+0011 V3 = DO_UCALL
+0012 T1 = ADD_ARRAY_ELEMENT V3 T4
+0013 JMP 0008
+0014 FE_FREE V2
+0015 FREE T1
+0016 RETURN int(1)
+LIVE RANGES:
+     1: 0007 - 0015 (tmp/var)
+     2: 0008 - 0014 (loop)
+     3: 0009 - 0010 (tmp/var)
+     4: 0009 - 0012 (tmp/var)
+
+stdClass:
+     ; (lines=3, args=0, vars=0, tmps=1)
+     ; (after optimizer)
+     ; %s
+0000 V0 = NEW 0 string("stdClass")
+0001 DO_FCALL
+0002 RETURN V0
+LIVE RANGES:
+     0: 0001 - 0002 (new)
diff --git a/ext/standard/tests/array/array_map_foreach_optimization_005.phpt b/ext/standard/tests/array/array_map_foreach_optimization_005.phpt
new file mode 100644
index 00000000000..6f7cf450213
--- /dev/null
+++ b/ext/standard/tests/array/array_map_foreach_optimization_005.phpt
@@ -0,0 +1,84 @@
+--TEST--
+array_map(): foreach optimization - static call
+--EXTENSIONS--
+opcache
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.opt_debug_level=0x20000
+--FILE--
+<?php
+
+class Adder {
+    static function plus1($x) {
+        return $x + 1;
+    }
+}
+
+$array = range(1, 10);
+
+$foo = array_map(Adder::plus1(...), $array);
+
+var_dump($foo);
+
+?>
+--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(10) 2
+0003 V2 = DO_ICALL
+0004 ASSIGN CV0($array) V2
+0005 TYPE_ASSERT 131079 string("array_map") CV0($array)
+0006 T2 = INIT_ARRAY 0 (packed) NEXT
+0007 V3 = FE_RESET_R CV0($array) 0014
+0008 T5 = FE_FETCH_R V3 T4 0014
+0009 INIT_STATIC_METHOD_CALL 1 string("Adder") string("plus1")
+0010 SEND_VAL T4 1
+0011 V4 = DO_UCALL
+0012 T2 = ADD_ARRAY_ELEMENT V4 T5
+0013 JMP 0008
+0014 FE_FREE V3
+0015 ASSIGN CV1($foo) T2
+0016 INIT_FCALL 1 %d string("var_dump")
+0017 SEND_VAR CV1($foo) 1
+0018 DO_ICALL
+0019 RETURN int(1)
+LIVE RANGES:
+     2: 0007 - 0015 (tmp/var)
+     3: 0008 - 0014 (loop)
+     4: 0009 - 0010 (tmp/var)
+     5: 0009 - 0012 (tmp/var)
+
+Adder::plus1:
+     ; (lines=3, args=1, vars=1, tmps=1)
+     ; (after optimizer)
+     ; %s
+0000 CV0($x) = RECV 1
+0001 T1 = ADD CV0($x) int(1)
+0002 RETURN T1
+array(10) {
+  [0]=>
+  int(2)
+  [1]=>
+  int(3)
+  [2]=>
+  int(4)
+  [3]=>
+  int(5)
+  [4]=>
+  int(6)
+  [5]=>
+  int(7)
+  [6]=>
+  int(8)
+  [7]=>
+  int(9)
+  [8]=>
+  int(10)
+  [9]=>
+  int(11)
+}