Commit 365d450baa9 for php
commit 365d450baa93f93336516bae9e574e53ae98d3c4
Author: ChrisAbner <48162172+ChrisAbner@users.noreply.github.com>
Date: Mon Nov 9 06:18:42 2026 -0500
Fix array_map() optimization not resolving the function name of an FCC (#25286)
diff --git a/Zend/tests/array_map_foreach_optimization_function_name.phpt b/Zend/tests/array_map_foreach_optimization_function_name.phpt
new file mode 100644
index 00000000000..ae9ea191dac
--- /dev/null
+++ b/Zend/tests/array_map_foreach_optimization_function_name.phpt
@@ -0,0 +1,127 @@
+--TEST--
+array_map(): foreach optimization resolves the function name of an FCC
+--FILE--
+<?php
+
+namespace {
+ function label($x) { return "global:$x"; }
+ function only_global($x) { return "only_global:$x"; }
+}
+
+namespace Lib {
+ function fmt($x) { return "Lib\\fmt:$x"; }
+}
+
+namespace App {
+ function label($x) { return "App\\label:$x"; }
+ function strtoupper($x) { return "App\\strtoupper:$x"; }
+ function pad($x, $y) { return "App\\pad:$x$y"; }
+
+ use function Lib\fmt;
+ use function App\label as lbl;
+
+ echo "namespaced function:\n";
+ var_dump(\array_map(label(...), [1]));
+ echo "namespaced function shadowing an internal one:\n";
+ var_dump(\array_map(strtoupper(...), ['a']));
+ echo "use function:\n";
+ var_dump(\array_map(fmt(...), [2]));
+ echo "use function with alias:\n";
+ var_dump(\array_map(lbl(...), [3]));
+ echo "namespace-relative name:\n";
+ var_dump(\array_map(namespace\label(...), [4]));
+ echo "fully qualified name:\n";
+ var_dump(\array_map(\label(...), [5]));
+ echo "fallback to the global function:\n";
+ var_dump(\array_map(only_global(...), [6]));
+ echo "fallback to an internal function:\n";
+ var_dump(\array_map(ucfirst(...), ['b']));
+ echo "partial application:\n";
+ var_dump(\array_map(pad(?, '!'), [7]));
+
+ if (true) {
+ function declared_at_runtime($x) { return "App\\declared_at_runtime:$x"; }
+ }
+ echo "function declared at runtime:\n";
+ var_dump(\array_map(declared_at_runtime(...), [8]));
+}
+
+namespace {
+ use function Lib\fmt;
+ use function Lib\fmt as libfmt;
+
+ echo "global namespace, use function:\n";
+ var_dump(array_map(fmt(...), [9]));
+ echo "global namespace, use function with alias:\n";
+ var_dump(array_map(libfmt(...), [10]));
+ echo "global namespace, unqualified:\n";
+ var_dump(array_map(label(...), [11]));
+}
+
+?>
+--EXPECT--
+namespaced function:
+array(1) {
+ [0]=>
+ string(11) "App\label:1"
+}
+namespaced function shadowing an internal one:
+array(1) {
+ [0]=>
+ string(16) "App\strtoupper:a"
+}
+use function:
+array(1) {
+ [0]=>
+ string(9) "Lib\fmt:2"
+}
+use function with alias:
+array(1) {
+ [0]=>
+ string(11) "App\label:3"
+}
+namespace-relative name:
+array(1) {
+ [0]=>
+ string(11) "App\label:4"
+}
+fully qualified name:
+array(1) {
+ [0]=>
+ string(8) "global:5"
+}
+fallback to the global function:
+array(1) {
+ [0]=>
+ string(13) "only_global:6"
+}
+fallback to an internal function:
+array(1) {
+ [0]=>
+ string(1) "B"
+}
+partial application:
+array(1) {
+ [0]=>
+ string(10) "App\pad:7!"
+}
+function declared at runtime:
+array(1) {
+ [0]=>
+ string(25) "App\declared_at_runtime:8"
+}
+global namespace, use function:
+array(1) {
+ [0]=>
+ string(9) "Lib\fmt:9"
+}
+global namespace, use function with alias:
+array(1) {
+ [0]=>
+ string(10) "Lib\fmt:10"
+}
+global namespace, unqualified:
+array(1) {
+ [0]=>
+ string(9) "global:11"
+}
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index 4de326b3677..9a0d330f04b 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -5399,9 +5399,19 @@ static zend_result zend_compile_func_array_map(znode *result, zend_ast_list *arg
/* Evaluate function name */
znode func_node;
- {
+ zend_ast *func_name_ast = callback->child[callback->kind == ZEND_AST_CALL ? 0 : 1];
+ if (callback->kind == ZEND_AST_CALL
+ && func_name_ast->kind == ZEND_AST_ZVAL
+ && Z_TYPE_P(zend_ast_get_zval(func_name_ast)) == IS_STRING) {
+ /* A literal function name has no side effects and must not be memoized:
+ * compiling it as an expression would yield the unresolved string, which
+ * ignores the current namespace, "use function" imports and the runtime
+ * fallback to the global function. Compile the call in the loop body with
+ * the original name instead, like a regular call. */
+ func_node.op_type = IS_UNUSED;
+ } else {
znode result;
- zend_compile_expr(&result, callback->child[callback->kind == ZEND_AST_CALL ? 0 : 1]);
+ zend_compile_expr(&result, func_name_ast);
if (result.op_type == IS_CONST || result.op_type == IS_TMP_VAR) {
func_node = result;
} else {
@@ -5459,7 +5469,9 @@ 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) {
+ if (func_node.op_type == IS_UNUSED) {
+ func_ast = func_name_ast;
+ } else if (func_node.op_type == IS_CONST) {
func_ast = zend_ast_create_znode(&func_node);
} else {
znode copy_node;
@@ -5486,7 +5498,9 @@ static zend_result zend_compile_func_array_map(znode *result, zend_ast_list *arg
break;
}
}
- zend_ast_destroy(func_ast);
+ if (func_ast != func_name_ast) {
+ 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 */
@@ -5501,7 +5515,7 @@ 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) {
+ if (func_node.op_type != IS_UNUSED && 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) {