Commit edc169e7705 for php.net

commit edc169e7705d5e4411865e9be92b50e82be78f4e
Author: Arnaud Le Blanc <365207+arnaud-lb@users.noreply.github.com>
Date:   Mon Jul 27 19:00:03 2026 +0200

    [PFA 4/n] Optimize constant pre-bound arguments  (#22829)

    Pre-bound arguments are bound to the generated closure's lexical vars:

    ```
    function f($a, $b) {}
    $f = f(1, ?);

    // Generates:
    $tmp = 1;
    $f = function ($b) use ($tmp) {
        return f($tmp, $b);
    };
    ```

    Detect which pre-bound arguments are constant and burn them into the generated
    closure instead:

    ```
    function f($a, $b) {}
    $f = f(1, ?);

    // Generates:
    $f = function ($b) {
        return f(1, $b);
    };
    ```

diff --git a/Zend/Optimizer/dfa_pass.c b/Zend/Optimizer/dfa_pass.c
index 77dc322fbde..dcb4e8bc841 100644
--- a/Zend/Optimizer/dfa_pass.c
+++ b/Zend/Optimizer/dfa_pass.c
@@ -469,6 +469,34 @@ static uint32_t zend_dfa_optimize_calls(zend_op_array *op_array, zend_ssa *ssa)
 					}
 				}
 			}
+
+			if (call_info->caller_call_opline && call_info->caller_call_opline->opcode == ZEND_CALLABLE_CONVERT_PARTIAL) {
+				/* Build a bitset of constant pre-bound PFA args: These are args whose value is alway the same for all
+				 * instances of a PFA. */
+				uint32_t const_args = 0;
+				for (uint32_t i = 0, l = MIN(sizeof(const_args)*CHAR_BIT, call_info->num_args); i < l; i++) {
+					zend_op *send_opline = call_info->arg_info[i].opline;
+					if (send_opline->op1_type == IS_CONST) {
+						zval *value = CT_CONSTANT_EX(op_array, send_opline->op1.constant);
+						if (Z_TYPE_P(value) == IS_CONSTANT_AST) {
+							/* Const exprs can evaluate to non-const zvals (e.g. objects), and are not idempotent */
+							continue;
+						}
+						const_args |= (UINT32_C(1) << i);
+					}
+				}
+
+				/* Pass the bitset to the ZEND_CALLABLE_CONVERT_PARTIAL opline. */
+				zend_op *call_opline = call_info->caller_call_opline;
+				if (call_opline->op2_type == IS_UNUSED) {
+					call_opline->op2.num = const_args;
+				} else {
+					ZEND_ASSERT(call_opline->op2_type == IS_CONST);
+					zval *zv = CT_CONSTANT_EX(op_array, call_opline->op2.constant);
+					Z_EXTRA_P(zv) = const_args;
+				}
+			}
+
 			call_info = call_info->next_callee;
 		} while (call_info);
 	}
diff --git a/Zend/tests/partial_application/const_arg_opt.phpt b/Zend/tests/partial_application/const_arg_opt.phpt
new file mode 100644
index 00000000000..d63456a96a5
--- /dev/null
+++ b/Zend/tests/partial_application/const_arg_opt.phpt
@@ -0,0 +1,162 @@
+--TEST--
+Constant argument optimization
+--DESCRIPTION--
+Pre-bound arguments that are constant can be burned into the generated
+op_array instead of being passed via the Closure's lexical vars.
+--ENV--
+A=1
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.optimization_level=-1
+--FILE--
+<?php
+
+function f($a, $b) {
+    var_dump([$a, $b]);
+}
+
+function g() {
+    return 3;
+}
+
+function h($a, $b, $c) {
+    var_dump([$a, $b, $c]);
+}
+
+function i($a1, $a2, $a3, $a4, $a5, $a6, $a7, $a8, $a9, $a10, $a11, $a12, $a13, $a14, $a15, $a16, $a17, $a18, $a19, $a20, $a21, $a22, $a23, $a24, $a25, $a26, $a27, $a28, $a29, $a30, $a31, $a32, $a33) {
+    var_dump($a33);
+}
+
+function print_lexical_vars($f) {
+    $vars = new ReflectionFunction($f)->getClosureUsedVariables();
+    if ($vars === []) {
+        echo "no lexical vars\n";
+    } else {
+        $varNames = array_keys($vars);
+        echo 'lexical vars: ', implode(', ', $varNames), "\n";
+    }
+}
+
+echo "# Non-constant pre-bound argument:\n";
+$f = f(getenv('A'), ?);
+print_lexical_vars($f);
+$f(2);
+
+echo "# Constant pre-bound argument:\n";
+$f = f(2, ?);
+print_lexical_vars($f);
+$f(2);
+
+echo "# Constant pre-bound argument (inverted):\n";
+$f = f(?, 2);
+print_lexical_vars($f);
+$f(1);
+
+echo "# Inlined pre-bound argument:\n";
+$f = f(g(), ?);
+print_lexical_vars($f);
+$f(2);
+
+echo "# Constexpr pre-bound argument:\n";
+const B = 4;
+$f = f(B, ?);
+print_lexical_vars($f);
+$f(2);
+
+echo "# Mixed arguments:\n";
+$f = h(5, getenv('A'), ?);
+print_lexical_vars($f);
+$f(2);
+
+echo "# Constant array pre-bound argument:\n";
+$f = f(['foo' => 'bar'], ?);
+print_lexical_vars($f);
+$f(2);
+
+echo "# Named arguments:\n";
+$f = h(1, c: ?, b: ?);
+print_lexical_vars($f);
+$f(2, 3);
+
+echo "# Many arguments (optimization can not be applied for all args) :\n";
+$f = i(?, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33);
+print_lexical_vars($f);
+$f(33);
+
+?>
+--EXPECT--
+# Non-constant pre-bound argument:
+lexical vars: a
+array(2) {
+  [0]=>
+  string(1) "1"
+  [1]=>
+  int(2)
+}
+# Constant pre-bound argument:
+no lexical vars
+array(2) {
+  [0]=>
+  int(2)
+  [1]=>
+  int(2)
+}
+# Constant pre-bound argument (inverted):
+no lexical vars
+array(2) {
+  [0]=>
+  int(1)
+  [1]=>
+  int(2)
+}
+# Inlined pre-bound argument:
+no lexical vars
+array(2) {
+  [0]=>
+  int(3)
+  [1]=>
+  int(2)
+}
+# Constexpr pre-bound argument:
+lexical vars: a
+array(2) {
+  [0]=>
+  int(4)
+  [1]=>
+  int(2)
+}
+# Mixed arguments:
+lexical vars: b
+array(3) {
+  [0]=>
+  int(5)
+  [1]=>
+  string(1) "1"
+  [2]=>
+  int(2)
+}
+# Constant array pre-bound argument:
+no lexical vars
+array(2) {
+  [0]=>
+  array(1) {
+    ["foo"]=>
+    string(3) "bar"
+  }
+  [1]=>
+  int(2)
+}
+# Named arguments:
+no lexical vars
+array(3) {
+  [0]=>
+  int(1)
+  [1]=>
+  int(3)
+  [2]=>
+  int(2)
+}
+# Many arguments (optimization can not be applied for all args) :
+lexical vars: a33
+int(33)
diff --git a/Zend/tests/partial_application/references_004.phpt b/Zend/tests/partial_application/references_004.phpt
index e5163c710da..8d56d7dceaf 100644
--- a/Zend/tests/partial_application/references_004.phpt
+++ b/Zend/tests/partial_application/references_004.phpt
@@ -1,5 +1,10 @@
 --TEST--
 PFA receives variadic param by ref if the actual function does
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.optimization_level=-1
+opcache.file_update_protection=0
 --FILE--
 <?php

@@ -26,9 +31,8 @@ function foo($a, &...$args) {
 Closure [ <user> static function {closure:%s:%d} ] {
   @@ %sreferences_004.php 13 - 13

-  - Bound Variables [2] {
-      Variable #0 [ $a ]
-      Variable #1 [ $args2 ]
+  - Bound Variables [1] {
+      Variable #0 [ $args2 ]
   }

   - Parameters [2] {
diff --git a/Zend/tests/partial_application/variation_debug_001.phpt b/Zend/tests/partial_application/variation_debug_001.phpt
index 04b63f3c401..cf215329f48 100644
--- a/Zend/tests/partial_application/variation_debug_001.phpt
+++ b/Zend/tests/partial_application/variation_debug_001.phpt
@@ -1,12 +1,19 @@
 --TEST--
 PFA variation: var_dump(), user function
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.optimization_level=-1
+opcache.file_update_protection=0
+--ENV--
+A=20
 --FILE--
 <?php
 function bar($a = 1, $b = 2, ...$c) {

 }

-var_dump(bar(?, new stdClass, 20, new stdClass, four: 4));
+var_dump(bar(?, new stdClass, (int)getenv('A'), new stdClass, four: 4));
 ?>
 --EXPECTF--
 object(Closure)#%d (5) {
diff --git a/Zend/tests/partial_application/variation_debug_002.phpt b/Zend/tests/partial_application/variation_debug_002.phpt
index a7c4c2d76e4..a4df317bbae 100644
--- a/Zend/tests/partial_application/variation_debug_002.phpt
+++ b/Zend/tests/partial_application/variation_debug_002.phpt
@@ -1,8 +1,20 @@
 --TEST--
 PFA variation: var_dump(), internal function
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.optimization_level=-1
+opcache.file_update_protection=0
+--ENV--
+A=1
 --FILE--
 <?php
-var_dump(array_map(?, [1, 2, 3], [4, 5, 6], four: new stdClass, ...));
+
+function notconst($value) {
+    return getenv('A') ? $value : null;
+}
+
+var_dump(array_map(?, notconst([1, 2, 3]), notconst([4, 5, 6]), four: new stdClass, ...));
 ?>
 --EXPECTF--
 object(Closure)#%d (5) {
@@ -11,7 +23,7 @@
   ["file"]=>
   string(%d) "%svariation_debug_002.php"
   ["line"]=>
-  int(2)
+  int(7)
   ["static"]=>
   array(3) {
     ["array"]=>
diff --git a/Zend/tests/partial_application/variation_variadics_001.phpt b/Zend/tests/partial_application/variation_variadics_001.phpt
index 850f0eda149..43b4ff23e29 100644
--- a/Zend/tests/partial_application/variation_variadics_001.phpt
+++ b/Zend/tests/partial_application/variation_variadics_001.phpt
@@ -1,5 +1,12 @@
 --TEST--
 PFA variation: variadics, user function
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.optimization_level=-1
+opcache.file_update_protection=0
+--ENV--
+A=1
 --FILE--
 <?php
 function foo($a, ...$b) {
@@ -16,11 +23,6 @@ function foo($a, ...$b) {
 Closure [ <user> static function {closure:%s:%d} ] {
   @@ %s 6 - 6

-  - Bound Variables [2] {
-      Variable #0 [ $a ]
-      Variable #1 [ $b2 ]
-  }
-
   - Parameters [1] {
     Parameter #0 [ <optional> ...$b ]
   }
diff --git a/Zend/tests/partial_application/variation_variadics_002.phpt b/Zend/tests/partial_application/variation_variadics_002.phpt
index 21d8169fc42..33330723f31 100644
--- a/Zend/tests/partial_application/variation_variadics_002.phpt
+++ b/Zend/tests/partial_application/variation_variadics_002.phpt
@@ -1,5 +1,10 @@
 --TEST--
 PFA variation: variadics, internal function
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.optimization_level=-1
+opcache.file_update_protection=0
 --FILE--
 <?php
 $sprintf = sprintf("%d %d %d", 100, ...);
@@ -12,11 +17,6 @@
 Closure [ <user> static function {closure:%s:%d} ] {
   @@ %svariation_variadics_002.php 2 - 2

-  - Bound Variables [2] {
-      Variable #0 [ $format ]
-      Variable #1 [ $values2 ]
-  }
-
   - Parameters [1] {
     Parameter #0 [ <optional> mixed ...$values ]
   }
diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c
index 8e735c6aba7..a17cb3d91b9 100644
--- a/Zend/zend_ast.c
+++ b/Zend/zend_ast.c
@@ -1352,7 +1352,7 @@ static zend_result ZEND_FASTCALL zend_ast_evaluate_inner(
 					ZEND_CALL_NUM_ARGS(frame), ZEND_CALL_ARG(frame, 1),
 					extra_named_params, named_positions,
 					fcc_ast->filename, &ast->lineno,
-					(void**)cache_slot, fcc_ast->name, flags);
+					(void**)cache_slot, fcc_ast->name, flags, 0);

 			if (named_positions) {
 				zend_array_release(named_positions);
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index 420a7cc0865..9c80cccb7cb 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -4162,6 +4162,8 @@ static void zend_compile_call_partial(znode *result, zend_ast_fcc *fcc_ast, uint
 	if (!Z_ISUNDEF_P(named_positions)) {
 		opline->op2.constant = zend_add_literal(named_positions);
 		opline->op2_type = IS_CONST;
+	} else {
+		opline->op2.num = 0;
 	}
 }

diff --git a/Zend/zend_partial.c b/Zend/zend_partial.c
index a78db3f51ef..ec60383a2bc 100644
--- a/Zend/zend_partial.c
+++ b/Zend/zend_partial.c
@@ -66,6 +66,14 @@ static zend_always_inline bool zp_is_non_static_closure(const zend_function *fun
 	return ((function->common.fn_flags & (ZEND_ACC_STATIC|ZEND_ACC_CLOSURE)) == ZEND_ACC_CLOSURE);
 }

+/* Whether argument at offset 'offset' is const. Such arguments can be burned into the generated op_array */
+static inline bool zp_is_const_arg(uint32_t const_args, uint32_t offset) {
+	if (offset < sizeof(const_args) * CHAR_BIT) {
+		return const_args & (UINT32_C(1) << offset);
+	}
+	return false;
+}
+
 static zend_never_inline ZEND_COLD void zp_args_underflow(
 		const zend_function *function, uint32_t args, uint32_t expected)
 {
@@ -179,7 +187,7 @@ static zend_string *zp_get_func_param_name(const zend_function *function, uint32
  * including params and used vars. */
 static zp_names *zp_assign_names(uint32_t argc, zval *argv,
 		zend_function *function, bool variadic_partial,
-		zend_array *extra_named_params)
+		zend_array *extra_named_params, uint32_t const_args)
 {
 	zp_names *names = zend_arena_calloc(&CG(ast_arena),
 			1, zend_safe_address_guarded(argc, sizeof(*names->params), offsetof(zp_names, params)));
@@ -226,7 +234,7 @@ static zp_names *zp_assign_names(uint32_t argc, zval *argv,
 	/* Assign names for pre-bound params (lexical vars).
 	 * There may be clashes, we ensure to generate unique names. */
 	for (uint32_t offset = 0; offset < argc; offset++) {
-		if (Z_IS_PLACEHOLDER_P(&argv[offset]) || Z_ISUNDEF(argv[offset])) {
+		if (Z_IS_PLACEHOLDER_P(&argv[offset]) || Z_ISUNDEF(argv[offset]) || zp_is_const_arg(const_args, offset)) {
 			continue;
 		}
 		uint32_t n = 2;
@@ -510,7 +518,7 @@ static zend_ast *zp_compile_forwarding_call(
 	zp_names *var_names, bool uses_variadic_placeholder, uint32_t num_args,
 	zend_class_entry *called_scope, zend_type return_type,
 	bool forward_superfluous_args,
-	zend_ast *stmts_ast)
+	zend_ast *stmts_ast, uint32_t const_args)
 {
 	bool is_assert = zend_string_equals(function->common.function_name,
 			ZSTR_KNOWN(ZEND_STR_ASSERT));
@@ -551,6 +559,9 @@ static zend_ast *zp_compile_forwarding_call(
 				default_value_ast = zend_ast_create_zval(&default_value);
 			}
 			args_ast = zend_ast_list_add(args_ast, default_value_ast);
+		} else if (zp_is_const_arg(const_args, offset)) {
+			ZEND_ASSERT(Z_TYPE(argv[offset]) < IS_OBJECT);
+			args_ast = zend_ast_list_add(args_ast, zend_ast_create_zval(&argv[offset]));
 		} else {
 			args_ast = zend_ast_list_add(args_ast, zend_ast_create(ZEND_AST_VAR,
 						zend_ast_create_zval_from_str(zend_string_copy(var_names->params[offset]))));
@@ -661,7 +672,7 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function,
 		const zend_array *named_positions,
 		zend_string *declaring_filename,
 		const uint32_t *declaring_lineno_ptr, void **cache_slot,
-		zend_string *pfa_name, uint32_t flags) {
+		zend_string *pfa_name, uint32_t flags, uint32_t const_args) {

 	zend_op_array *op_array = NULL;

@@ -771,7 +782,7 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function,
 	/* Assign variable names */

 	zp_names *var_names = zp_assign_names(argc, argv, function,
-			uses_variadic_placeholder, extra_named_params);
+			uses_variadic_placeholder, extra_named_params, const_args);

 	/* Generate AST */

@@ -825,15 +836,17 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function,
 						default_value_ast, attributes_ast, NULL, NULL);

 			} else if (!Z_ISUNDEF(argv[offset])) {
-				// TODO: If the pre-bound parameter is a literal, it can be a
-				// literal in the function body instead of a lexical var.
-				zend_ast *lexical_var_ast = zend_ast_create_zval_from_str(
-						zend_string_copy(var_names->params[offset]));
-				if (zp_arg_must_be_sent_by_ref(function, offset+1)) {
-					lexical_var_ast->attr = ZEND_BIND_REF;
+				if (zp_is_const_arg(const_args, offset)) {
+					/* Will be burned into the op_array */
+				} else {
+					zend_ast *lexical_var_ast = zend_ast_create_zval_from_str(
+							zend_string_copy(var_names->params[offset]));
+					if (zp_arg_must_be_sent_by_ref(function, offset+1)) {
+						lexical_var_ast->attr = ZEND_BIND_REF;
+					}
+					lexical_vars_ast = zend_ast_list_add(
+							lexical_vars_ast, lexical_var_ast);
 				}
-				lexical_vars_ast = zend_ast_list_add(
-						lexical_vars_ast, lexical_var_ast);
 			}
 		}

@@ -894,7 +907,7 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function,
 		no_forwarding_ast = zp_compile_forwarding_call(this_ptr, function,
 				argc, argv, extra_named_params,
 				var_names, uses_variadic_placeholder, num_params,
-				called_scope, return_type, false, no_forwarding_ast);
+				called_scope, return_type, false, no_forwarding_ast, const_args);

 		if (!no_forwarding_ast) {
 			ZEND_ASSERT(EG(exception));
@@ -904,7 +917,7 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function,
 		forwarding_ast = zp_compile_forwarding_call(this_ptr, function,
 				argc, argv, extra_named_params,
 				var_names, uses_variadic_placeholder, num_params,
-				called_scope, return_type, true, forwarding_ast);
+				called_scope, return_type, true, forwarding_ast, const_args);

 		if (!forwarding_ast) {
 			ZEND_ASSERT(EG(exception));
@@ -931,7 +944,7 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function,
 		stmts_ast = zp_compile_forwarding_call(this_ptr, function,
 				argc, argv, extra_named_params,
 				var_names, uses_variadic_placeholder, num_params,
-				called_scope, return_type, false, stmts_ast);
+				called_scope, return_type, false, stmts_ast, const_args);

 		if (!stmts_ast) {
 			ZEND_ASSERT(EG(exception));
@@ -1000,7 +1013,7 @@ static const zend_op_array *zp_get_op_array(zval *this_ptr, zend_function *funct
 		const zend_array *named_positions,
 		zend_string *declaring_filename,
 		const uint32_t *declaring_lineno_ptr, void **cache_slot,
-		zend_string *pfa_name, uint32_t flags) {
+		zend_string *pfa_name, uint32_t flags, uint32_t const_args) {

 	if (EXPECTED(function->type == ZEND_INTERNAL_FUNCTION
 					? cache_slot[0] == function
@@ -1015,7 +1028,7 @@ static const zend_op_array *zp_get_op_array(zval *this_ptr, zend_function *funct
 	if (UNEXPECTED(!op_array)) {
 		op_array = zp_compile(this_ptr, function, argc, argv,
 			extra_named_params, named_positions, declaring_filename, declaring_lineno_ptr,
-			cache_slot, pfa_name, flags);
+			cache_slot, pfa_name, flags, const_args);
 	}

 	if (EXPECTED(op_array) && !(function->common.fn_flags & ZEND_ACC_NEVER_CACHE)) {
@@ -1037,7 +1050,7 @@ static void zp_free_unbound_args(uint32_t start, uint32_t argc, zval *argv)

 /* Bind pre-bound arguments as lexical vars */
 static void zp_bind(zval *result, zend_function *function, uint32_t argc, zval *argv,
-		zend_array *extra_named_params) {
+		zend_array *extra_named_params, uint32_t const_args) {

 	zend_arg_info *arg_infos = function->common.arg_info;
 	uint32_t bind_offset = 0;
@@ -1052,7 +1065,7 @@ static void zp_bind(zval *result, zend_function *function, uint32_t argc, zval *

 	for (uint32_t offset = 0; offset < argc; offset++) {
 		zval *var = &argv[offset];
-		if (Z_IS_PLACEHOLDER_P(var) || Z_ISUNDEF_P(var)) {
+		if (Z_IS_PLACEHOLDER_P(var) || Z_ISUNDEF_P(var) || zp_is_const_arg(const_args, offset)) {
 			continue;
 		}
 		zend_arg_info *arg_info;
@@ -1095,14 +1108,14 @@ void zend_partial_create(zval *result, zval *this_ptr, zend_function *function,
 		const zend_array *named_positions,
 		zend_string *declaring_filename,
 		const uint32_t *declaring_lineno_ptr, void **cache_slot,
-		zend_string *pfa_name, uint32_t flags) {
+		zend_string *pfa_name, uint32_t flags, uint32_t const_args) {

 	ZEND_ASSERT(pfa_name);

 	const zend_op_array *op_array = zp_get_op_array(this_ptr, function, argc, argv,
 			extra_named_params, named_positions,
 			declaring_filename, declaring_lineno_ptr,
-			cache_slot, pfa_name, flags);
+			cache_slot, pfa_name, flags, const_args);

 	if (UNEXPECTED(!op_array)) {
 		ZEND_ASSERT(EG(exception));
@@ -1130,7 +1143,7 @@ void zend_partial_create(zval *result, zval *this_ptr, zend_function *function,
 			function->common.scope, called_scope, &object,
 			(function->common.fn_flags & ZEND_ACC_CLOSURE) != 0);

-	zp_bind(result, function, argc, argv, extra_named_params);
+	zp_bind(result, function, argc, argv, extra_named_params, const_args);
 }

 void zend_partial_op_array_dtor(zval *pDest)
diff --git a/Zend/zend_partial.h b/Zend/zend_partial.h
index 386823261e2..d3fcdae6afc 100644
--- a/Zend/zend_partial.h
+++ b/Zend/zend_partial.h
@@ -36,7 +36,7 @@ void zend_partial_create(zval *result, zval *this_ptr, zend_function *function,
 		const zend_array *named_positions,
 		zend_string *declaring_filename,
 		const uint32_t *declaring_lineno_ptr, void **cache_slot,
-		zend_string *pfa_name, uint32_t flags);
+		zend_string *pfa_name, uint32_t flags, uint32_t const_args);

 void zend_partial_op_array_dtor(zval *pDest);

diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h
index 3466bc68d69..d14230514b3 100644
--- a/Zend/zend_vm_def.h
+++ b/Zend/zend_vm_def.h
@@ -9875,6 +9875,13 @@ ZEND_VM_HANDLER(212, ZEND_CALLABLE_CONVERT_PARTIAL, CONST, CONST|UNUSED, NUM)
 	void **cache_slot = CACHE_ADDR(opline->extended_value & ~ZEND_PARTIAL_FLAGS);
 	zval *named_positions = GET_OP2_ZVAL_PTR();
 	zend_string *pfa_name = Z_STR_P(GET_OP1_ZVAL_PTR());
+	uint32_t const_args;
+
+	if (OP2_TYPE == IS_UNUSED) {
+		const_args = opline->op2.num;
+	} else {
+		const_args = Z_EXTRA_P(named_positions);
+	}

 	zend_partial_create(EX_VAR(opline->result.var),
 		&call->This, call->func,
@@ -9883,7 +9890,7 @@ ZEND_VM_HANDLER(212, ZEND_CALLABLE_CONVERT_PARTIAL, CONST, CONST|UNUSED, NUM)
 			call->extra_named_params : NULL,
 		OP2_TYPE == IS_CONST ? Z_ARRVAL_P(named_positions) : NULL,
 		EX(func)->op_array.filename, &opline->lineno, cache_slot,
-		pfa_name, opline->extended_value & ZEND_PARTIAL_FLAGS);
+		pfa_name, opline->extended_value & ZEND_PARTIAL_FLAGS, const_args);

 	if (ZEND_CALL_INFO(call) & ZEND_CALL_HAS_EXTRA_NAMED_PARAMS) {
 		zend_array_release(call->extra_named_params);
diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h
index c0a23e49d02..53bcdccd971 100644
Binary files a/Zend/zend_vm_execute.h and b/Zend/zend_vm_execute.h differ