Commit b5fba859eaa for php.net

commit b5fba859eaa72b14c7c8bc08e4a6d5ce9774e7fd
Author: Arnaud Le Blanc <365207+arnaud-lb@users.noreply.github.com>
Date:   Fri Jul 17 10:09:22 2026 +0200

    Partial function application (#20848)

    RFC: https://wiki.php.net/rfc/partial_function_application_v2

    Co-authored-by: Joe Watkins <krakjoe@php.net>

diff --git a/NEWS b/NEWS
index e1a8e55de7b..b63ae8a81c9 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,9 @@ PHP                                                                        NEWS
 |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
 ?? ??? ????, PHP 8.6.0alpha3

+- Core:
+  . Implemented partial function application RFC. (Arnaud)
+
 - GMP:
   . Fixed GMP power and shift operators to reject GMP right operands outside
     the unsigned long range instead of silently truncating them. (Weilin Du)
diff --git a/UPGRADING b/UPGRADING
index 26e50b215f7..145f2239b56 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -235,6 +235,8 @@ PHP 8.6 UPGRADE NOTES
     RFC: https://wiki.php.net/rfc/debugable-enums
   . #[\Override] can now be applied to class constants, including enum cases.
     RFC: https://wiki.php.net/rfc/override_constants
+  . Implemented partial function application
+    RFC: https://wiki.php.net/rfc/partial_function_application_v2

 - Curl:
   . curl_getinfo() return array now includes a new size_delivered key, which
diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS
index cca78d1d3b4..fd6d009aaff 100644
--- a/UPGRADING.INTERNALS
+++ b/UPGRADING.INTERNALS
@@ -132,6 +132,10 @@ PHP 8.6 INTERNALS UPGRADE NOTES
   . zend_argument_error_variadic() now takes a new 'function' parameters.
   . Added zend_argument_error_ex(), zend_argument_type_error_ex(),
     zend_argument_value_error_ex().
+  . Added zend_ast_dup().
+  . Added zend_compile_ast().
+  . Added zend_check_type_ex().
+  . Added zend_create_partial_closure().

 ========================
 2. Build system changes
diff --git a/Zend/Optimizer/compact_literals.c b/Zend/Optimizer/compact_literals.c
index cf74dd8fc14..a4ecb19c85e 100644
--- a/Zend/Optimizer/compact_literals.c
+++ b/Zend/Optimizer/compact_literals.c
@@ -733,6 +733,7 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
 				case ZEND_SEND_VAR_NO_REF_EX:
 				case ZEND_SEND_REF:
 				case ZEND_SEND_FUNC_ARG:
+				case ZEND_SEND_PLACEHOLDER:
 				case ZEND_CHECK_FUNC_ARG:
 					if (opline->op2_type == IS_CONST) {
 						opline->result.num = cache_size;
@@ -745,6 +746,10 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
 						cache_size += sizeof(void *);
 					}
 					break;
+				case ZEND_CALLABLE_CONVERT_PARTIAL:
+					opline->op1.num = cache_size;
+					cache_size += 2 * sizeof(void *);
+					break;
 			}
 			opline++;
 		}
diff --git a/Zend/Optimizer/optimize_func_calls.c b/Zend/Optimizer/optimize_func_calls.c
index 69c371207dd..05cdce4fc4c 100644
--- a/Zend/Optimizer/optimize_func_calls.c
+++ b/Zend/Optimizer/optimize_func_calls.c
@@ -191,6 +191,7 @@ void zend_optimize_func_calls(zend_op_array *op_array, zend_optimizer_ctx *ctx)
 			case ZEND_DO_UCALL:
 			case ZEND_DO_FCALL_BY_NAME:
 			case ZEND_CALLABLE_CONVERT:
+			case ZEND_CALLABLE_CONVERT_PARTIAL:
 				call--;
 				if (call_stack[call].func && call_stack[call].opline) {
 					zend_op *fcall = call_stack[call].opline;
@@ -223,13 +224,14 @@ void zend_optimize_func_calls(zend_op_array *op_array, zend_optimizer_ctx *ctx)
 					 * At this point we also know whether or not the result of
 					 * the DO opcode is used, allowing to optimize calls to
 					 * ZEND_ACC_NODISCARD functions. */
-					if (opline->opcode != ZEND_CALLABLE_CONVERT) {
+					if (opline->opcode != ZEND_CALLABLE_CONVERT && opline->opcode != ZEND_CALLABLE_CONVERT_PARTIAL) {
 						opline->opcode = zend_get_call_op(fcall, call_stack[call].func, !RESULT_UNUSED(opline));
 					}

 					if ((ZEND_OPTIMIZER_PASS_16 & ctx->optimization_level)
 							&& call_stack[call].try_inline
-							&& opline->opcode != ZEND_CALLABLE_CONVERT) {
+							&& opline->opcode != ZEND_CALLABLE_CONVERT
+							&& opline->opcode != ZEND_CALLABLE_CONVERT_PARTIAL) {
 						zend_try_inline_call(op_array, fcall, opline, call_stack[call].func);
 					}
 				}
diff --git a/Zend/Optimizer/zend_call_graph.c b/Zend/Optimizer/zend_call_graph.c
index bb80e21a246..b67f57cf041 100644
--- a/Zend/Optimizer/zend_call_graph.c
+++ b/Zend/Optimizer/zend_call_graph.c
@@ -124,6 +124,7 @@ ZEND_API void zend_analyze_calls(zend_arena **arena, zend_script *script, uint32
 			case ZEND_DO_UCALL:
 			case ZEND_DO_FCALL_BY_NAME:
 			case ZEND_CALLABLE_CONVERT:
+			case ZEND_CALLABLE_CONVERT_PARTIAL:
 				func_info->flags |= ZEND_FUNC_HAS_CALLS;
 				if (call_info) {
 					call_info->caller_call_opline = opline;
@@ -140,6 +141,7 @@ ZEND_API void zend_analyze_calls(zend_arena **arena, zend_script *script, uint32
 			case ZEND_SEND_VAR_NO_REF:
 			case ZEND_SEND_VAR_NO_REF_EX:
 			case ZEND_SEND_USER:
+			case ZEND_SEND_PLACEHOLDER:
 				if (call_info) {
 					if (opline->op2_type == IS_CONST) {
 						call_info->named_args = true;
diff --git a/Zend/Optimizer/zend_inference.c b/Zend/Optimizer/zend_inference.c
index 05d33d3d75f..2e6cc70ec25 100644
--- a/Zend/Optimizer/zend_inference.c
+++ b/Zend/Optimizer/zend_inference.c
@@ -3903,6 +3903,7 @@ static zend_always_inline zend_result _zend_update_type_info(
 			}
 			break;
 		case ZEND_CALLABLE_CONVERT:
+		case ZEND_CALLABLE_CONVERT_PARTIAL:
 			UPDATE_SSA_TYPE(MAY_BE_OBJECT | MAY_BE_RC1 | MAY_BE_RCN, ssa_op->result_def);
 			UPDATE_SSA_OBJ_TYPE(zend_ce_closure, /* is_instanceof */ false, ssa_op->result_def);
 			break;
diff --git a/Zend/tests/first_class_callable/first_class_callable_non_unary_error.phpt b/Zend/tests/first_class_callable/first_class_callable_non_unary_error.phpt
deleted file mode 100644
index 74e36a9ad0d..00000000000
--- a/Zend/tests/first_class_callable/first_class_callable_non_unary_error.phpt
+++ /dev/null
@@ -1,10 +0,0 @@
---TEST--
-First class callable error: more than one argument
---FILE--
-<?php
-
-foo(1, ...);
-
-?>
---EXPECTF--
-Fatal error: Cannot create a Closure for call expression with more than one argument, or non-variadic placeholders in %s on line %d
diff --git a/Zend/tests/first_class_callable/first_class_callable_non_variadic_error.phpt b/Zend/tests/first_class_callable/first_class_callable_non_variadic_error.phpt
deleted file mode 100644
index efbd13b7593..00000000000
--- a/Zend/tests/first_class_callable/first_class_callable_non_variadic_error.phpt
+++ /dev/null
@@ -1,10 +0,0 @@
---TEST--
-First class callable error: non-variadic placeholder
---FILE--
-<?php
-
-foo(?);
-
-?>
---EXPECTF--
-Fatal error: Cannot create a Closure for call expression with more than one argument, or non-variadic placeholders in %s on line %d
diff --git a/Zend/tests/partial_application/assert.phpt b/Zend/tests/partial_application/assert.phpt
new file mode 100644
index 00000000000..166cc5022eb
--- /dev/null
+++ b/Zend/tests/partial_application/assert.phpt
@@ -0,0 +1,45 @@
+--TEST--
+PFA of assert() behaves like a dynamic call to assert()
+--FILE--
+<?php
+
+try {
+    echo "# Static call:\n";
+    assert(false);
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+try {
+    echo "# Dynamic call:\n";
+    (function ($f) { $f(false); })('assert');
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage() ?: '(no message)', "\n";
+}
+
+try {
+    echo "# PFA call:\n";
+    $f = assert(?);
+    $f(false);
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage() ?: '(no message)', "\n";
+}
+
+try {
+    echo "# Upper-case assert():\n";
+    $f = ASSERT(?);
+    $f(false);
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage() ?: '(no message)', "\n";
+}
+
+?>
+--EXPECT--
+# Static call:
+AssertionError: assert(false)
+# Dynamic call:
+AssertionError: (no message)
+# PFA call:
+AssertionError: (no message)
+# Upper-case assert():
+AssertionError: (no message)
diff --git a/Zend/tests/partial_application/attributes_001.phpt b/Zend/tests/partial_application/attributes_001.phpt
new file mode 100644
index 00000000000..827ad41321e
--- /dev/null
+++ b/Zend/tests/partial_application/attributes_001.phpt
@@ -0,0 +1,88 @@
+--TEST--
+PFA inherits NoDiscard and SensitiveParameter attributes
+--FILE--
+<?php
+
+#[Attribute]
+class Test {}
+
+#[NoDiscard] #[Test]
+function f($a, #[SensitiveParameter] $b, #[Test] ...$c) {
+}
+
+function dump_attributes($function) {
+    $r = new ReflectionFunction($function);
+    var_dump($r->getAttributes());
+
+    foreach ($r->getParameters() as $i => $p) {
+        echo "Parameter $i:\n";
+        var_dump($p->getAttributes());
+    }
+}
+
+echo "# Orig attributes:\n";
+
+dump_attributes('f');
+
+$f = f(1, ?, ?, ...);
+
+echo "# PFA attributes:\n";
+
+dump_attributes($f);
+
+?>
+--EXPECTF--
+# Orig attributes:
+array(2) {
+  [0]=>
+  object(ReflectionAttribute)#%d (1) {
+    ["name"]=>
+    string(9) "NoDiscard"
+  }
+  [1]=>
+  object(ReflectionAttribute)#%d (1) {
+    ["name"]=>
+    string(4) "Test"
+  }
+}
+Parameter 0:
+array(0) {
+}
+Parameter 1:
+array(1) {
+  [0]=>
+  object(ReflectionAttribute)#%d (1) {
+    ["name"]=>
+    string(18) "SensitiveParameter"
+  }
+}
+Parameter 2:
+array(1) {
+  [0]=>
+  object(ReflectionAttribute)#%d (1) {
+    ["name"]=>
+    string(4) "Test"
+  }
+}
+# PFA attributes:
+array(1) {
+  [0]=>
+  object(ReflectionAttribute)#%d (1) {
+    ["name"]=>
+    string(9) "NoDiscard"
+  }
+}
+Parameter 0:
+array(1) {
+  [0]=>
+  object(ReflectionAttribute)#%d (1) {
+    ["name"]=>
+    string(18) "SensitiveParameter"
+  }
+}
+Parameter 1:
+array(0) {
+}
+Parameter 2:
+array(0) {
+}
diff --git a/Zend/tests/partial_application/attributes_002.phpt b/Zend/tests/partial_application/attributes_002.phpt
new file mode 100644
index 00000000000..be1f1612f93
--- /dev/null
+++ b/Zend/tests/partial_application/attributes_002.phpt
@@ -0,0 +1,19 @@
+--TEST--
+PFA preserves #[SensitiveParameter]
+--FILE--
+<?php
+
+function f($a, #[SensitiveParameter] $b, $c, #[SensitiveParameter] ...$d) {
+    throw new Exception();
+}
+
+$f = f(1, ?, ?, ?, ...)('normal param', 3, 'reified variadic', 'variadic');
+
+?>
+--EXPECTF--
+Fatal error: Uncaught Exception in %s:%d
+Stack trace:
+#0 %s(%d): f(1, Object(SensitiveParameterValue), 3, Object(SensitiveParameterValue), Object(SensitiveParameterValue))
+#1 %s(%d): {closure:pfa:%s:7}(Object(SensitiveParameterValue), 3, Object(SensitiveParameterValue), Object(SensitiveParameterValue))
+#2 {main}
+  thrown in %s on line %d
diff --git a/Zend/tests/partial_application/attributes_003.phpt b/Zend/tests/partial_application/attributes_003.phpt
new file mode 100644
index 00000000000..d113d02eb8f
--- /dev/null
+++ b/Zend/tests/partial_application/attributes_003.phpt
@@ -0,0 +1,16 @@
+--TEST--
+PFA preserves #[NoDiscard]
+--FILE--
+<?php
+
+#[NoDiscard] function f($a) {
+}
+
+$f = f(?);
+$f(1);
+
+(void) $f(1);
+
+?>
+--EXPECTF--
+Warning: The return value of function {closure:%s}() should either be used or intentionally ignored by casting it as (void) in %s on line 7
diff --git a/Zend/tests/partial_application/clone.phpt b/Zend/tests/partial_application/clone.phpt
new file mode 100644
index 00000000000..f778d776b71
--- /dev/null
+++ b/Zend/tests/partial_application/clone.phpt
@@ -0,0 +1,50 @@
+--TEST--
+clone() can be partially applied
+--FILE--
+<?php
+
+class C {
+    public function __construct(
+        public mixed $a,
+        public mixed $b,
+    ) { }
+}
+
+$clone = clone(?);
+var_dump($clone(new C(1, 2)));
+
+$clone = clone(...);
+var_dump($clone(new C(3, 4)));
+
+$clone = clone(new C(5, 6), ?);
+var_dump($clone(['a' => 7]));
+
+$clone = clone(?, ['a' => 8]);
+var_dump($clone(new C(9, 10)));
+
+?>
+--EXPECTF--
+object(C)#%d (2) {
+  ["a"]=>
+  int(1)
+  ["b"]=>
+  int(2)
+}
+object(C)#%d (2) {
+  ["a"]=>
+  int(3)
+  ["b"]=>
+  int(4)
+}
+object(C)#%d (2) {
+  ["a"]=>
+  int(7)
+  ["b"]=>
+  int(6)
+}
+object(C)#%d (2) {
+  ["a"]=>
+  int(8)
+  ["b"]=>
+  int(10)
+}
diff --git a/Zend/tests/partial_application/compile_errors_001.phpt b/Zend/tests/partial_application/compile_errors_001.phpt
new file mode 100644
index 00000000000..f08495a1f1e
--- /dev/null
+++ b/Zend/tests/partial_application/compile_errors_001.phpt
@@ -0,0 +1,8 @@
+--TEST--
+PFA compile errors: multiple variadic placeholders
+--FILE--
+<?php
+foo(..., ...);
+?>
+--EXPECTF--
+Fatal error: Variadic placeholder may only appear once in %s on line %d
diff --git a/Zend/tests/partial_application/compile_errors_002.phpt b/Zend/tests/partial_application/compile_errors_002.phpt
new file mode 100644
index 00000000000..b6a2073a836
--- /dev/null
+++ b/Zend/tests/partial_application/compile_errors_002.phpt
@@ -0,0 +1,8 @@
+--TEST--
+PFA compile errors: variadic placeholder must be last
+--FILE--
+<?php
+foo(..., ?);
+?>
+--EXPECTF--
+Fatal error: Variadic placeholder must be last in %s on line %d
diff --git a/Zend/tests/partial_application/compile_errors_003.phpt b/Zend/tests/partial_application/compile_errors_003.phpt
new file mode 100644
index 00000000000..26ff8435111
--- /dev/null
+++ b/Zend/tests/partial_application/compile_errors_003.phpt
@@ -0,0 +1,8 @@
+--TEST--
+PFA compile errors: placeholders can not appear after named args
+--FILE--
+<?php
+foo(n: 5, ?);
+?>
+--EXPECTF--
+Fatal error: Cannot use positional argument after named argument in %s on line %d
diff --git a/Zend/tests/partial_application/compile_errors_004.phpt b/Zend/tests/partial_application/compile_errors_004.phpt
new file mode 100644
index 00000000000..ac7ec163c5d
--- /dev/null
+++ b/Zend/tests/partial_application/compile_errors_004.phpt
@@ -0,0 +1,8 @@
+--TEST--
+PFA compile errors: variadic placeholder must be last, including after named args
+--FILE--
+<?php
+foo(..., n: 5);
+?>
+--EXPECTF--
+Fatal error: Variadic placeholder must be last in %s on line %d
diff --git a/Zend/tests/partial_application/compile_errors_005.phpt b/Zend/tests/partial_application/compile_errors_005.phpt
new file mode 100644
index 00000000000..30e4aa12b48
--- /dev/null
+++ b/Zend/tests/partial_application/compile_errors_005.phpt
@@ -0,0 +1,8 @@
+--TEST--
+PFA compile errors: variadic placeholder must be last, including after positional args
+--FILE--
+<?php
+foo(..., $a);
+?>
+--EXPECTF--
+Fatal error: Variadic placeholder must be last in %s on line %d
diff --git a/Zend/tests/partial_application/compile_errors_006.phpt b/Zend/tests/partial_application/compile_errors_006.phpt
new file mode 100644
index 00000000000..8549c671e90
--- /dev/null
+++ b/Zend/tests/partial_application/compile_errors_006.phpt
@@ -0,0 +1,8 @@
+--TEST--
+PFA compile errors: can not use unpacking in PFA, including with variadic placeholders
+--FILE--
+<?php
+foo(...["foo" => "bar"], ...);
+?>
+--EXPECTF--
+Fatal error: Cannot combine partial application and unpacking in %s on line %d
diff --git a/Zend/tests/partial_application/deprecated.phpt b/Zend/tests/partial_application/deprecated.phpt
new file mode 100644
index 00000000000..5a6280347cf
--- /dev/null
+++ b/Zend/tests/partial_application/deprecated.phpt
@@ -0,0 +1,14 @@
+--TEST--
+PFAs may emit deprecation warnings
+--FILE--
+<?php
+
+#[Deprecated] function f($a) {
+}
+
+$f = f(?);
+$f(1);
+
+?>
+--EXPECTF--
+Deprecated: Function f() is deprecated in %s on line %d
diff --git a/Zend/tests/partial_application/errors_001.phpt b/Zend/tests/partial_application/errors_001.phpt
new file mode 100644
index 00000000000..2d5348cb28a
--- /dev/null
+++ b/Zend/tests/partial_application/errors_001.phpt
@@ -0,0 +1,62 @@
+--TEST--
+PFA errors: PFA instantiation follows the usual argument count validation
+--FILE--
+<?php
+function foo($a, $b, $c) {
+
+}
+
+class C {
+    function f($a, $b, $c) {
+    }
+}
+
+try {
+    foo(?);
+} catch (Error $ex) {
+    printf("%s: %s\n", $ex::class, $ex->getMessage());
+}
+
+try {
+    foo(?, ?, ?, ?);
+} catch (Error $ex) {
+    printf("%s: %s\n", $ex::class, $ex->getMessage());
+}
+
+try {
+    $c = new C();
+    $c->f(?);
+} catch (Error $ex) {
+    printf("%s: %s\n", $ex::class, $ex->getMessage());
+}
+
+try {
+    property_exists(?);
+} catch (Error $ex) {
+    printf("%s: %s\n", $ex::class, $ex->getMessage());
+}
+
+try {
+    usleep(?, ?);
+} catch (Error $ex) {
+    printf("%s: %s\n", $ex::class, $ex->getMessage());
+}
+
+try {
+    foo(?, ?, ?, ?, ...);
+} catch (Error $ex) {
+    printf("%s: %s\n", $ex::class, $ex->getMessage());
+}
+
+/* It is allowed to specify less than the number of required params, when there
+ * is a variadic placeholder */
+foo(?, ...);
+
+?>
+--EXPECT--
+ArgumentCountError: Partial application of foo() expects exactly 3 arguments, 1 given
+ArgumentCountError: Partial application of foo() expects at most 3 arguments, 4 given
+ArgumentCountError: Partial application of C::f() expects exactly 3 arguments, 1 given
+ArgumentCountError: Partial application of property_exists() expects exactly 2 arguments, 1 given
+ArgumentCountError: Partial application of usleep() expects at most 1 arguments, 2 given
+ArgumentCountError: Partial application of foo() expects at most 3 arguments, 4 given
diff --git a/Zend/tests/partial_application/errors_002.phpt b/Zend/tests/partial_application/errors_002.phpt
new file mode 100644
index 00000000000..0132d8873e2
--- /dev/null
+++ b/Zend/tests/partial_application/errors_002.phpt
@@ -0,0 +1,15 @@
+--TEST--
+PFA errors: named parameter that resolve to the position of a placeholder is an error
+--FILE--
+<?php
+function foo($a) {
+}
+
+try {
+    foo(?, a: 1);
+} catch (Error $ex) {
+    printf("%s: %s\n", $ex::class, $ex->getMessage());
+}
+?>
+--EXPECT--
+Error: Named parameter $a overwrites previous placeholder
diff --git a/Zend/tests/partial_application/errors_003.phpt b/Zend/tests/partial_application/errors_003.phpt
new file mode 100644
index 00000000000..85ecf398fa1
--- /dev/null
+++ b/Zend/tests/partial_application/errors_003.phpt
@@ -0,0 +1,74 @@
+--TEST--
+PFA errors: PFA call follows the usual argument count validation
+--FILE--
+<?php
+function foo($a, ...$b) {
+
+}
+
+function bar($a, $b, $c) {}
+
+$foo = foo(?);
+
+try {
+    $foo();
+} catch (Error $ex) {
+    printf("%s: %s\n", $ex::class, $ex->getMessage());
+}
+
+$foo = foo(?, ?);
+
+try {
+    $foo(1);
+} catch (Error $ex) {
+    printf("%s: %s\n", $ex::class, $ex->getMessage());
+}
+
+$bar = bar(?, ?, ...);
+
+try {
+    $bar(1);
+} catch (Error $ex) {
+    printf("%s: %s\n", $ex::class, $ex->getMessage());
+}
+
+class Foo {
+    public function bar($a, ...$b) {}
+}
+
+$foo = new Foo;
+
+$bar = $foo->bar(?);
+
+try {
+    $bar();
+} catch (Error $ex) {
+    printf("%s: %s\n", $ex::class, $ex->getMessage());
+}
+
+$repeat = str_repeat('a', ...);
+
+try {
+    $repeat();
+} catch (Error $ex) {
+    printf("%s: %s\n", $ex::class, $ex->getMessage());
+}
+
+$usleep = usleep(?);
+
+try {
+    $usleep();
+} catch (Error $ex) {
+    printf("%s: %s\n", $ex::class, $ex->getMessage());
+}
+
+$usleep(1, 2);
+
+?>
+--EXPECTF--
+ArgumentCountError: Too few arguments to function {closure:%s:%d}(), 0 passed in %s on line %d and exactly 1 expected
+ArgumentCountError: Too few arguments to function {closure:%s:%d}(), 1 passed in %s on line %d and exactly 2 expected
+ArgumentCountError: Too few arguments to function {closure:%s:%d}(), 1 passed in %s on line %d and exactly 3 expected
+ArgumentCountError: Too few arguments to function Foo::{closure:%s:%d}(), 0 passed in %s on line %d and exactly 1 expected
+ArgumentCountError: Too few arguments to function {closure:%s:%d}(), 0 passed in %s on line %d and exactly 1 expected
+ArgumentCountError: Too few arguments to function {closure:%s:%d}(), 0 passed in %s on line %d and exactly 1 expected
diff --git a/Zend/tests/partial_application/errors_004.phpt b/Zend/tests/partial_application/errors_004.phpt
new file mode 100644
index 00000000000..e5e1432753b
--- /dev/null
+++ b/Zend/tests/partial_application/errors_004.phpt
@@ -0,0 +1,16 @@
+--TEST--
+PFA errors: not specifying a required param is an error
+--FILE--
+<?php
+
+function f($a, $b, $c = null) {
+}
+
+$f = f(1, c: ?);
+
+?>
+--EXPECTF--
+Fatal error: Uncaught ArgumentCountError: f(): Argument #2 ($b) not passed in %s:6
+Stack trace:
+#0 {main}
+  thrown in %s on line %d
diff --git a/Zend/tests/partial_application/errors_005.phpt b/Zend/tests/partial_application/errors_005.phpt
new file mode 100644
index 00000000000..23fc2a75dff
--- /dev/null
+++ b/Zend/tests/partial_application/errors_005.phpt
@@ -0,0 +1,20 @@
+--TEST--
+PFA errors: Can not fetch default parameter value for Closure::__invoke()
+--FILE--
+<?php
+
+$f = function ($a, $b, $c = null) {};
+
+$g = $f->__invoke(0, 0, ?);
+echo "No error\n";
+
+try {
+    $g = $f->__invoke(0, 0, ...);
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+No error
+ArgumentCountError: Closure::__invoke(): Argument #3 ($c) must be passed explicitly, because the default value is not known
diff --git a/Zend/tests/partial_application/errors_006.phpt b/Zend/tests/partial_application/errors_006.phpt
new file mode 100644
index 00000000000..9534bb40ee5
--- /dev/null
+++ b/Zend/tests/partial_application/errors_006.phpt
@@ -0,0 +1,45 @@
+--TEST--
+PFA errors: Some internal function parameters have UNKNOWN default value
+--FILE--
+<?php
+
+$f = array_keys(?, 42);
+var_dump($f([41, 42, 43]));
+
+$f = array_keys(?, '42', true);
+var_dump($f([41, 42, 43]));
+
+$f = array_keys(?, '42', strict: ?);
+var_dump($f([41, 42, 43], false));
+var_dump($f([41, 42, 43], true));
+
+try {
+    // fn (array $array) => array_keys($array, ???, true)
+    $f = array_keys(?, strict: true);
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+try {
+    // fn (array $array, mixed $filter_value = ???) => array_keys($array, $filter_value, true)
+    $f = array_keys(?, strict: true, ...);
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+array(1) {
+  [0]=>
+  int(1)
+}
+array(0) {
+}
+array(1) {
+  [0]=>
+  int(1)
+}
+array(0) {
+}
+ArgumentCountError: array_keys(): Argument #2 ($filter_value) must be passed explicitly, because the default value is not known
+ArgumentCountError: array_keys(): Argument #2 ($filter_value) must be passed explicitly, because the default value is not known
diff --git a/Zend/tests/partial_application/export_001.phpt b/Zend/tests/partial_application/export_001.phpt
new file mode 100644
index 00000000000..b48146bcc65
--- /dev/null
+++ b/Zend/tests/partial_application/export_001.phpt
@@ -0,0 +1,14 @@
+--TEST--
+PFA AST export
+--INI--
+assert.exception=1
+--FILE--
+<?php
+try {
+    assert(0 && foo(?) && foo(new stdClass, bar: 1, ...));
+} catch (Error $ex) {
+    printf("%s: %s\n", $ex::class, $ex->getMessage());
+}
+?>
+--EXPECT--
+AssertionError: assert(0 && foo(?) && foo(new stdClass(), bar: 1, ...))
diff --git a/Zend/tests/partial_application/extra_named.phpt b/Zend/tests/partial_application/extra_named.phpt
new file mode 100644
index 00000000000..4dd80cfa012
--- /dev/null
+++ b/Zend/tests/partial_application/extra_named.phpt
@@ -0,0 +1,49 @@
+--TEST--
+PFA extra named parameters are forwarded to the actual function
+--FILE--
+<?php
+function foo(...$args) {
+    var_dump($args);
+}
+
+$foo = foo(foo: "foo", ...);
+
+$bar = $foo(bar: "bar", ...);
+
+$baz = $bar(baz: "baz", ...);
+
+$baz();
+
+$foo = foo(...);
+
+$bar = $foo(bar: "bar", ...);
+
+$baz = $bar(baz: "baz", ...);
+
+$baz();
+
+$foo = foo(foo: "foo", ...);
+
+$foo(bar: "bar");
+?>
+--EXPECT--
+array(3) {
+  ["foo"]=>
+  string(3) "foo"
+  ["bar"]=>
+  string(3) "bar"
+  ["baz"]=>
+  string(3) "baz"
+}
+array(2) {
+  ["bar"]=>
+  string(3) "bar"
+  ["baz"]=>
+  string(3) "baz"
+}
+array(2) {
+  ["foo"]=>
+  string(3) "foo"
+  ["bar"]=>
+  string(3) "bar"
+}
diff --git a/Zend/tests/partial_application/function_name.phpt b/Zend/tests/partial_application/function_name.phpt
new file mode 100644
index 00000000000..68d2ccf8487
--- /dev/null
+++ b/Zend/tests/partial_application/function_name.phpt
@@ -0,0 +1,40 @@
+--TEST--
+Partial application function name
+--FILE--
+<?php
+
+function g($a) {}
+
+class C {
+    static function m() {
+        echo "# From a method:\n";
+        var_dump((new ReflectionFunction(g(?)))->getName());
+    }
+}
+
+function f() {
+    echo "# From a function:\n";
+    var_dump((new ReflectionFunction(g(?)))->getName());
+
+    echo "# Declared on same line:\n";
+    [$a, $b] = [g(?), g(?)];
+    var_dump((new ReflectionFunction($a))->getName(), (new ReflectionFunction($b))->getName());
+}
+
+f();
+C::m();
+
+echo "# From global scope:\n";
+var_dump((new ReflectionFunction(g(?)))->getName());
+
+?>
+--EXPECTF--
+# From a function:
+string(20) "{closure:pfa:f():14}"
+# Declared on same line:
+string(20) "{closure:pfa:f():17}"
+string(20) "{closure:pfa:f():17}"
+# From a method:
+string(22) "{closure:pfa:C::m():8}"
+# From global scope:
+string(%d) "{closure:pfa:%sfunction_name.php:25}"
diff --git a/Zend/tests/partial_application/fuzz_001.phpt b/Zend/tests/partial_application/fuzz_001.phpt
new file mode 100644
index 00000000000..790162ba6a3
--- /dev/null
+++ b/Zend/tests/partial_application/fuzz_001.phpt
@@ -0,0 +1,21 @@
+--TEST--
+Closure application fuzz 001
+--FILE--
+<?php
+$closure = function($a, $b) {};
+
+echo (string) new ReflectionFunction($closure(1, ?));
+?>
+--EXPECTF--
+Closure [ <user> function {closure:%s:%d} ] {
+  @@ %s 4 - 4
+
+  - Bound Variables [2] {
+      Variable #0 [ $fn ]
+      Variable #1 [ $a ]
+  }
+
+  - Parameters [1] {
+    Parameter #0 [ <required> $b ]
+  }
+}
diff --git a/Zend/tests/partial_application/fuzz_003.phpt b/Zend/tests/partial_application/fuzz_003.phpt
new file mode 100644
index 00000000000..6e9d583fda9
--- /dev/null
+++ b/Zend/tests/partial_application/fuzz_003.phpt
@@ -0,0 +1,20 @@
+--TEST--
+Closure application fuzz 003
+--FILE--
+<?php
+class Foo {
+    function __call($name, $args) {
+        var_dump($args);
+    }
+}
+$foo = new Foo;
+$bar = $foo->method(1, ...);
+$bar(2);
+?>
+--EXPECT--
+array(2) {
+  [0]=>
+  int(1)
+  [1]=>
+  int(2)
+}
diff --git a/Zend/tests/partial_application/fuzz_004.phpt b/Zend/tests/partial_application/fuzz_004.phpt
new file mode 100644
index 00000000000..ea005304a3a
--- /dev/null
+++ b/Zend/tests/partial_application/fuzz_004.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Closure application fuzz 004
+--FILE--
+<?php
+function foo($a, $b) {
+    return $a + $b;
+}
+
+$foo = foo(b: 10, ...);
+
+try {
+    $foo->__invoke(UNDEFINED);
+} catch (\Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+Error: Undefined constant "UNDEFINED"
diff --git a/Zend/tests/partial_application/fuzz_005.phpt b/Zend/tests/partial_application/fuzz_005.phpt
new file mode 100644
index 00000000000..ea04862d839
--- /dev/null
+++ b/Zend/tests/partial_application/fuzz_005.phpt
@@ -0,0 +1,16 @@
+--TEST--
+PFA fuzz 005
+--FILE--
+<?php
+
+function foo(int $day = 1, int $month = 1, int $yearh = 1, int $year = 2005) {
+}
+
+$foo = foo(month: 12, ...);
+
+$bar = $foo(year: 2016, ...);
+
+?>
+==DONE==
+--EXPECT--
+==DONE==
diff --git a/Zend/tests/partial_application/fuzz_006.phpt b/Zend/tests/partial_application/fuzz_006.phpt
new file mode 100644
index 00000000000..26ec6e3e4dd
--- /dev/null
+++ b/Zend/tests/partial_application/fuzz_006.phpt
@@ -0,0 +1,16 @@
+--TEST--
+PFA fuzz 006
+--FILE--
+<?php
+
+function foo(int $day = 1, int $month = 1, int $year = 2005) {
+    var_dump($day);
+}
+
+$foo = foo(month: 12, ...);
+
+$foo(year: 2025);
+
+?>
+--EXPECT--
+int(1)
diff --git a/Zend/tests/partial_application/fuzz_007.phpt b/Zend/tests/partial_application/fuzz_007.phpt
new file mode 100644
index 00000000000..123ce29d8b1
--- /dev/null
+++ b/Zend/tests/partial_application/fuzz_007.phpt
@@ -0,0 +1,19 @@
+--TEST--
+PFA fuzz 007
+--FILE--
+<?php
+
+function foo(int $day = 1, int $month = UNDEFINED, int $year = 2005) {
+}
+
+$foo = foo(year: 2006, ...);
+
+try {
+    $foo(2);
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+Undefined constant "UNDEFINED"
diff --git a/Zend/tests/partial_application/hook.phpt b/Zend/tests/partial_application/hook.phpt
new file mode 100644
index 00000000000..6402c5d01e7
--- /dev/null
+++ b/Zend/tests/partial_application/hook.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Parent property hook call can not be partially applied
+--FILE--
+<?php
+
+class C {
+    public $a {
+        set($value) { var_dump($value); }
+    }
+}
+
+class D extends C {
+    public $a {
+        set($value) {
+            $f = parent::$a::set(?);
+        }
+    }
+}
+
+$d = new D();
+$d->a = 1;
+
+?>
+--EXPECTF--
+Fatal error: Cannot create Closure for parent property hook call in %s on line %d
diff --git a/Zend/tests/partial_application/instance_polymorphism.phpt b/Zend/tests/partial_application/instance_polymorphism.phpt
new file mode 100644
index 00000000000..59fb5795d60
--- /dev/null
+++ b/Zend/tests/partial_application/instance_polymorphism.phpt
@@ -0,0 +1,43 @@
+--TEST--
+PFA: instance polymorphism
+--FILE--
+<?php
+
+class P {
+    public function m(string $a): void {
+        echo __METHOD__, PHP_EOL;
+        var_dump($a);
+    }
+
+
+    public function get() {
+        /* The method is resolved before creating the PFA, and captured by the
+         * PFA. We use the signature of the resolved method. */
+        return $this->m(?);
+    }
+}
+
+class C extends P {
+    public function m(string|array $b): void {
+        echo __METHOD__, PHP_EOL;
+        var_dump($b);
+    }
+}
+
+for ($i = 0; $i < 2; $i++) {
+    (new P())->get()(a: 'a');
+    (new C())->get()(b: []);
+}
+
+?>
+--EXPECT--
+P::m
+string(1) "a"
+C::m
+array(0) {
+}
+P::m
+string(1) "a"
+C::m
+array(0) {
+}
diff --git a/Zend/tests/partial_application/invokable.phpt b/Zend/tests/partial_application/invokable.phpt
new file mode 100644
index 00000000000..c21030e7733
--- /dev/null
+++ b/Zend/tests/partial_application/invokable.phpt
@@ -0,0 +1,51 @@
+--TEST--
+__invoke() can be partially applied
+--FILE--
+<?php
+
+class C {
+    public function __invoke(int $a, object $b): C {
+        var_dump($a, $b);
+        return $this;
+    }
+}
+
+$c = new C();
+$f = $c(?, ...);
+
+echo (string) new ReflectionFunction($f), "\n";
+
+$f = $c(?, new stdClass);
+
+echo (string) new ReflectionFunction($f), "\n";
+
+$f(1);
+
+?>
+--EXPECTF--
+Closure [ <user> public method {closure:%s:%d} ] {
+  @@ %s.php 11 - 11
+
+  - Parameters [2] {
+    Parameter #0 [ <required> int $a ]
+    Parameter #1 [ <required> object $b ]
+  }
+  - Return [ C ]
+}
+
+Closure [ <user> public method {closure:%s:%d} ] {
+  @@ %s.php 15 - 15
+
+  - Bound Variables [1] {
+      Variable #0 [ $b ]
+  }
+
+  - Parameters [1] {
+    Parameter #0 [ <required> int $a ]
+  }
+  - Return [ C ]
+}
+
+int(1)
+object(stdClass)#%d (0) {
+}
diff --git a/Zend/tests/partial_application/jit_001.phpt b/Zend/tests/partial_application/jit_001.phpt
new file mode 100644
index 00000000000..84aefa05ab2
--- /dev/null
+++ b/Zend/tests/partial_application/jit_001.phpt
@@ -0,0 +1,9 @@
+--TEST--
+PFA JIT 001
+--FILE--
+<?php
+(function ($a,$b) { var_dump($a, $b); })(1, ...)(2);
+?>
+--EXPECT--
+int(1)
+int(2)
diff --git a/Zend/tests/partial_application/magic_001.phpt b/Zend/tests/partial_application/magic_001.phpt
new file mode 100644
index 00000000000..bdcc1067578
--- /dev/null
+++ b/Zend/tests/partial_application/magic_001.phpt
@@ -0,0 +1,81 @@
+--TEST--
+__call() can be partially applied
+--FILE--
+<?php
+class Foo {
+    public function __call($method, $arguments) {
+        printf("%s::%s\n", __CLASS__, $method);
+
+        var_dump(...$arguments);
+    }
+}
+
+$foo = new Foo;
+
+$bar = $foo->method(?);
+
+echo (string) new ReflectionFunction($bar);
+
+try {
+    $bar();
+} catch (Error $ex) {
+    printf("%s: %s\n", $ex::class, $ex->getMessage());
+}
+
+$bar(1, 2);
+$bar(1);
+
+$bar = $foo->method(?, ...);
+
+echo (string) new ReflectionFunction($bar);
+
+$bar(10);
+$bar(10, 20);
+
+$bar = $foo->method(new Foo, ...);
+
+echo (string) new ReflectionFunction($bar);
+
+$bar(100);
+?>
+--EXPECTF--
+Closure [ <user> public method {closure:%s:%d} ] {
+  @@ %s 12 - 12
+
+  - Parameters [1] {
+    Parameter #0 [ <required> mixed $arguments0 ]
+  }
+}
+ArgumentCountError: Too few arguments to function Foo::{closure:%s:%d}(), 0 passed in %s on line %d and exactly 1 expected
+Foo::method
+int(1)
+Foo::method
+int(1)
+Closure [ <user> public method {closure:%s:%d} ] {
+  @@ %s 25 - 25
+
+  - Parameters [2] {
+    Parameter #0 [ <required> mixed $arguments0 ]
+    Parameter #1 [ <optional> mixed ...$arguments ]
+  }
+}
+Foo::method
+int(10)
+Foo::method
+int(10)
+int(20)
+Closure [ <user> public method {closure:%s:%d} ] {
+  @@ %s 32 - 32
+
+  - Bound Variables [1] {
+      Variable #0 [ $arguments2 ]
+  }
+
+  - Parameters [1] {
+    Parameter #0 [ <optional> mixed ...$arguments ]
+  }
+}
+Foo::method
+object(Foo)#%d (0) {
+}
+int(100)
diff --git a/Zend/tests/partial_application/magic_002.phpt b/Zend/tests/partial_application/magic_002.phpt
new file mode 100644
index 00000000000..1d5efaea7c6
--- /dev/null
+++ b/Zend/tests/partial_application/magic_002.phpt
@@ -0,0 +1,72 @@
+--TEST--
+__callStatic() can be partially applied
+--FILE--
+<?php
+class Foo {
+    public static function __callStatic($method, $arguments) {
+        printf("%s::%s\n", __CLASS__, $method);
+
+        var_dump(...$arguments);
+    }
+}
+
+$bar = Foo::method(?);
+
+echo (string) new ReflectionFunction($bar);
+
+$bar(1);
+$bar(1, 2);
+
+$bar = Foo::method(?, ...);
+
+echo (string) new ReflectionFunction($bar);
+
+$bar(10);
+$bar(10, 20);
+
+$bar = Foo::method(new Foo,...);
+
+echo (string) new ReflectionFunction($bar);
+
+$bar(100);
+?>
+--EXPECTF--
+Closure [ <user> static public method {closure:%s:%d} ] {
+  @@ %s 10 - 10
+
+  - Parameters [1] {
+    Parameter #0 [ <required> mixed $arguments0 ]
+  }
+}
+Foo::method
+int(1)
+Foo::method
+int(1)
+Closure [ <user> static public method {closure:%s:%d} ] {
+  @@ %s 17 - 17
+
+  - Parameters [2] {
+    Parameter #0 [ <required> mixed $arguments0 ]
+    Parameter #1 [ <optional> mixed ...$arguments ]
+  }
+}
+Foo::method
+int(10)
+Foo::method
+int(10)
+int(20)
+Closure [ <user> static public method {closure:%s:%d} ] {
+  @@ %s 24 - 24
+
+  - Bound Variables [1] {
+      Variable #0 [ $arguments2 ]
+  }
+
+  - Parameters [1] {
+    Parameter #0 [ <optional> mixed ...$arguments ]
+  }
+}
+Foo::method
+object(Foo)#%d (0) {
+}
+int(100)
diff --git a/Zend/tests/partial_application/magic_003.phpt b/Zend/tests/partial_application/magic_003.phpt
new file mode 100644
index 00000000000..242403ea38f
--- /dev/null
+++ b/Zend/tests/partial_application/magic_003.phpt
@@ -0,0 +1,13 @@
+--TEST--
+PFA magic trampoline release (result unused)
+--FILE--
+<?php
+class Foo {
+    static function __callStatic($name, $args) {}
+}
+Foo::method(?);
+
+echo "OK";
+?>
+--EXPECT--
+OK
diff --git a/Zend/tests/partial_application/magic_004.phpt b/Zend/tests/partial_application/magic_004.phpt
new file mode 100644
index 00000000000..8ef93a91ce8
--- /dev/null
+++ b/Zend/tests/partial_application/magic_004.phpt
@@ -0,0 +1,14 @@
+--TEST--
+PFA magic trampoline release (result used)
+--FILE--
+<?php
+class Foo {
+    static function __callStatic($name, $args) {}
+}
+$a = Foo::method(?);
+(void)Foo::method(?);
+
+echo "OK";
+?>
+--EXPECT--
+OK
diff --git a/Zend/tests/partial_application/magic_005.phpt b/Zend/tests/partial_application/magic_005.phpt
new file mode 100644
index 00000000000..e70ed90b176
--- /dev/null
+++ b/Zend/tests/partial_application/magic_005.phpt
@@ -0,0 +1,29 @@
+--TEST--
+PFA magic null ptr deref in arginfo
+--FILE--
+<?php
+class Foo {
+    function __call($name, $args) {
+    }
+}
+$foo = new Foo;
+$bar = $foo->method(?);
+var_dump($bar);
+?>
+--EXPECTF--
+object(Closure)#%d (%d) {
+  ["name"]=>
+  string(%d) "{closure:%s}"
+  ["file"]=>
+  string(%d) "%smagic_005.php"
+  ["line"]=>
+  int(7)
+  ["this"]=>
+  object(Foo)#%d (0) {
+  }
+  ["parameter"]=>
+  array(1) {
+    ["$arguments0"]=>
+    string(10) "<required>"
+  }
+}
diff --git a/Zend/tests/partial_application/magic_006.phpt b/Zend/tests/partial_application/magic_006.phpt
new file mode 100644
index 00000000000..29564d8b615
--- /dev/null
+++ b/Zend/tests/partial_application/magic_006.phpt
@@ -0,0 +1,20 @@
+--TEST--
+PFA magic varargs
+--FILE--
+<?php
+class Foo {
+    function __call($name, $args) {
+        var_dump($args);
+    }
+}
+$foo = new Foo;
+$bar = $foo->method(1,...);
+$bar(2);
+?>
+--EXPECT--
+array(2) {
+  [0]=>
+  int(1)
+  [1]=>
+  int(2)
+}
diff --git a/Zend/tests/partial_application/named_placeholders.phpt b/Zend/tests/partial_application/named_placeholders.phpt
new file mode 100644
index 00000000000..19ce8cc0386
--- /dev/null
+++ b/Zend/tests/partial_application/named_placeholders.phpt
@@ -0,0 +1,121 @@
+--TEST--
+PFA supports named placeholders
+--FILE--
+<?php
+
+class A {}
+class B {}
+class C {}
+
+function foo($a = 1, $b = 2, $c = 3) {
+    var_dump($a, $b, $c);
+}
+
+echo "# Case 1\n";
+
+$c = foo(b: ?);
+
+echo (string) new ReflectionFunction($c);
+
+$c(new B);
+
+echo "# Case 2\n";
+
+$c = $c(?);
+
+echo (string) new ReflectionFunction($c);
+
+$c(new B);
+
+echo "# Case 3\n";
+
+$c = foo(?, ?);
+try {
+    $c = $c(b: ?);
+} catch (\Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+echo "# Case 4\n";
+
+function bar($a = 1, $b = 2, ...$c) {
+    var_dump($a, $b, $c);
+}
+
+$d = bar(b: ?, ...);
+
+echo (string) new ReflectionFunction($d);
+
+$d(new B, new A, new C);
+
+echo "# Case 5\n";
+
+try {
+    $d = bar(?, a: ?);
+} catch (\Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+echo "# Case 6\n";
+
+try {
+    $d = bar(c: ?, ...);
+} catch (\Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+# Case 1
+Closure [ <user> static function {closure:%s:%d} ] {
+  @@ %snamed_placeholders.php 13 - 13
+
+  - Parameters [1] {
+    Parameter #0 [ <required> $b ]
+  }
+}
+int(1)
+object(B)#%d (0) {
+}
+int(3)
+# Case 2
+Closure [ <user> static function {closure:%s:%d} ] {
+  @@ %snamed_placeholders.php 21 - 21
+
+  - Bound Variables [1] {
+      Variable #0 [ $fn ]
+  }
+
+  - Parameters [1] {
+    Parameter #0 [ <required> $b ]
+  }
+}
+int(1)
+object(B)#%d (0) {
+}
+int(3)
+# Case 3
+ArgumentCountError: {closure:pfa:%s:29}(): Argument #1 ($a) not passed
+# Case 4
+Closure [ <user> static function {closure:%s:%d} ] {
+  @@ %snamed_placeholders.php 42 - 42
+
+  - Parameters [3] {
+    Parameter #0 [ <required> $b ]
+    Parameter #1 [ <optional> $a = 1 ]
+    Parameter #2 [ <optional> ...$c ]
+  }
+}
+object(A)#%d (0) {
+}
+object(B)#%d (0) {
+}
+array(1) {
+  [0]=>
+  object(C)#%d (0) {
+  }
+}
+# Case 5
+Error: Named parameter $a overwrites previous placeholder
+# Case 6
+Error: Cannot use named placeholder for unknown or variadic parameter $c
diff --git a/Zend/tests/partial_application/never_cache_001.phpt b/Zend/tests/partial_application/never_cache_001.phpt
new file mode 100644
index 00000000000..3c9e8a173e5
--- /dev/null
+++ b/Zend/tests/partial_application/never_cache_001.phpt
@@ -0,0 +1,30 @@
+--TEST--
+Inline cache can not be used for ZEND_ACC_NEVER_CACHE functions
+--FILE--
+<?php
+
+$fns = [
+    'return function ($x) { var_dump("A"); };',
+    'return function ($x) { var_dump("B"); };',
+];
+
+function f($f) {
+    $f(0);
+}
+
+foreach ($fns as $f) {
+    f(eval($f));
+}
+
+foreach ($fns as $f) {
+    $f = eval($f);
+    // Closure::__invoke() is ZEND_ACC_NEVER_CACHE
+    f($f->__invoke(?));
+}
+
+?>
+--EXPECT--
+string(1) "A"
+string(1) "B"
+string(1) "A"
+string(1) "B"
diff --git a/Zend/tests/partial_application/non_dynamic_call_funcs.phpt b/Zend/tests/partial_application/non_dynamic_call_funcs.phpt
new file mode 100644
index 00000000000..db25ef3dcc9
--- /dev/null
+++ b/Zend/tests/partial_application/non_dynamic_call_funcs.phpt
@@ -0,0 +1,46 @@
+--TEST--
+Functions that can not be called dynamically, can not be partially applied
+--FILE--
+<?php
+
+function _func_get_arg() {
+    return func_get_arg(?);
+}
+
+function _compact() {
+    return compact(?);
+}
+
+function _extract() {
+    return extract(?);
+}
+
+function _func_get_args() {
+    return func_get_args(?);
+}
+
+function _func_num_args() {
+    return func_num_args(?);
+}
+
+function _get_defined_vars() {
+    return get_defined_vars(?);
+}
+
+foreach (['_func_get_arg', '_compact', '_extract', '_func_get_args', '_func_num_args', '_get_defined_vars'] as $func) {
+    try {
+        $func();
+        echo "$func\n";
+    } catch (Error $e) {
+        echo $e::class, ": ", $e->getMessage(), "\n";
+    }
+}
+
+?>
+--EXPECT--
+Error: Cannot call func_get_arg() dynamically
+Error: Cannot call compact() dynamically
+Error: Cannot call extract() dynamically
+Error: Cannot call func_get_args() dynamically
+Error: Cannot call func_num_args() dynamically
+Error: Cannot call get_defined_vars() dynamically
diff --git a/Zend/tests/partial_application/observers.phpt b/Zend/tests/partial_application/observers.phpt
new file mode 100644
index 00000000000..8717b38ca32
--- /dev/null
+++ b/Zend/tests/partial_application/observers.phpt
@@ -0,0 +1,34 @@
+--TEST--
+PFA support observers
+--EXTENSIONS--
+zend_test
+--INI--
+zend_test.observer.enabled=1
+zend_test.observer.show_output=1
+zend_test.observer.observe_all=1
+--FILE--
+<?php
+
+function f($a, $b) {
+    var_dump($a, $b);
+}
+
+$f = f(1, ...);
+$f(2);
+
+?>
+--EXPECTF--
+<!-- init '%sobservers.php' -->
+<file '%sobservers.php'>
+  <!-- init {closure:%s}() -->
+  <{closure:%s}>
+    <!-- init f() -->
+    <f>
+      <!-- init var_dump() -->
+      <var_dump>
+int(1)
+int(2)
+      </var_dump>
+    </f>
+  </{closure:%s:%d}>
+</file '%sobservers.php'>
diff --git a/Zend/tests/partial_application/optional_placeholder.phpt b/Zend/tests/partial_application/optional_placeholder.phpt
new file mode 100644
index 00000000000..d6230241d90
--- /dev/null
+++ b/Zend/tests/partial_application/optional_placeholder.phpt
@@ -0,0 +1,77 @@
+--TEST--
+PFA optional placeholder: ? always generates required params; ... inherits optionality
+--FILE--
+<?php
+
+function foo(int $a = 1, string $b = 'default', float $c = 3.14) {
+    return [$a, $b, $c];
+}
+
+echo "# ? makes targeted params required regardless of original optionality\n";
+$f = foo(?);
+var_dump((new ReflectionFunction($f))->getParameters()[0]->isOptional());
+
+$f = foo(?, ?);
+$params = (new ReflectionFunction($f))->getParameters();
+var_dump($params[0]->isOptional(), $params[1]->isOptional());
+
+$f = foo(?, ?, ?);
+$params = (new ReflectionFunction($f))->getParameters();
+var_dump($params[0]->isOptional(), $params[1]->isOptional(), $params[2]->isOptional());
+
+echo "# ... inherits optionality from the original function\n";
+$f = foo(?, ...);
+$params = (new ReflectionFunction($f))->getParameters();
+var_dump($params[0]->isOptional(), $params[1]->isOptional(), $params[2]->isOptional());
+
+$f = foo(?, ?, ...);
+$params = (new ReflectionFunction($f))->getParameters();
+var_dump($params[0]->isOptional(), $params[1]->isOptional(), $params[2]->isOptional());
+
+echo "# Calling works: ? params are required, unspecified optional params use their defaults\n";
+$f = foo(?, ?);
+var_dump($f(10, 'hello'));
+
+echo "# Named ? placeholders are also required\n";
+
+function bar(int $x = 0, int $y = 0) {
+    return [$x, $y];
+}
+
+$f = bar(y: ?);
+var_dump((new ReflectionFunction($f))->getParameters()[0]->isOptional());
+var_dump($f(42));
+
+?>
+--EXPECT--
+# ? makes targeted params required regardless of original optionality
+bool(false)
+bool(false)
+bool(false)
+bool(false)
+bool(false)
+bool(false)
+# ... inherits optionality from the original function
+bool(false)
+bool(true)
+bool(true)
+bool(false)
+bool(false)
+bool(true)
+# Calling works: ? params are required, unspecified optional params use their defaults
+array(3) {
+  [0]=>
+  int(10)
+  [1]=>
+  string(5) "hello"
+  [2]=>
+  float(3.14)
+}
+# Named ? placeholders are also required
+bool(false)
+array(2) {
+  [0]=>
+  int(0)
+  [1]=>
+  int(42)
+}
diff --git a/Zend/tests/partial_application/param_reorder.phpt b/Zend/tests/partial_application/param_reorder.phpt
new file mode 100644
index 00000000000..3ade1beb0af
--- /dev/null
+++ b/Zend/tests/partial_application/param_reorder.phpt
@@ -0,0 +1,202 @@
+--TEST--
+Named parameters define the order of parameters in a PFA
+--FILE--
+<?php
+
+function f($a, $b, $c, $d = null) {
+    return [$a, $b, $c, $d];
+}
+
+function g($a, $b, $c, ...$d) {
+    return [$a, $b, $c, $d];
+}
+
+echo "# All named\n";
+
+$a = f(d: ?, c: ?, b: ?, a: ?);
+echo new ReflectionFunction($a), "\n";
+var_dump($a(1, 2, 3, 4));
+
+echo "# Some named: Positional first, then named in specified order\n";
+
+$a = f(?, ?, d: ?, c: ?);
+echo new ReflectionFunction($a), "\n";
+var_dump($a(1, 2, 3, 4));
+
+echo "# Some named, one unspecified\n";
+
+$a = f(?, c: ?, b: ?);
+echo new ReflectionFunction($a), "\n";
+var_dump($a(1, 2, 3, 4));
+
+echo "# Some named, some implicit added by '...'\n";
+
+$a = f(c: ?, b: ?, ...);
+echo new ReflectionFunction($a), "\n";
+var_dump($a(1, 2, 3, 4));
+
+echo "# Some named, some implicit added by '...' on variadic function\n";
+
+$a = g(c: ?, b: ?, ...);
+echo new ReflectionFunction($a), "\n";
+var_dump($a(1, 2, 3, 4, 5, 6));
+
+echo "# Some prebound, some named\n";
+
+$a = f(-1, c: ?, d: -2, b: ?);
+echo new ReflectionFunction($a), "\n";
+var_dump($a(1, 2));
+
+echo "# Some named, some required missing\n";
+
+try {
+    $a = f(b: ?, c: ?);
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+# All named
+Closure [ <user> static function {closure:%s:%d} ] {
+  @@ %sparam_reorder.php 13 - 13
+
+  - Parameters [4] {
+    Parameter #0 [ <required> $d ]
+    Parameter #1 [ <required> $c ]
+    Parameter #2 [ <required> $b ]
+    Parameter #3 [ <required> $a ]
+  }
+}
+
+array(4) {
+  [0]=>
+  int(4)
+  [1]=>
+  int(3)
+  [2]=>
+  int(2)
+  [3]=>
+  int(1)
+}
+# Some named: Positional first, then named in specified order
+Closure [ <user> static function {closure:%s:%d} ] {
+  @@ %sparam_reorder.php 19 - 19
+
+  - Parameters [4] {
+    Parameter #0 [ <required> $a ]
+    Parameter #1 [ <required> $b ]
+    Parameter #2 [ <required> $d ]
+    Parameter #3 [ <required> $c ]
+  }
+}
+
+array(4) {
+  [0]=>
+  int(1)
+  [1]=>
+  int(2)
+  [2]=>
+  int(4)
+  [3]=>
+  int(3)
+}
+# Some named, one unspecified
+Closure [ <user> static function {closure:%s:%d} ] {
+  @@ %sparam_reorder.php 25 - 25
+
+  - Parameters [3] {
+    Parameter #0 [ <required> $a ]
+    Parameter #1 [ <required> $c ]
+    Parameter #2 [ <required> $b ]
+  }
+}
+
+array(4) {
+  [0]=>
+  int(1)
+  [1]=>
+  int(3)
+  [2]=>
+  int(2)
+  [3]=>
+  NULL
+}
+# Some named, some implicit added by '...'
+Closure [ <user> static function {closure:%s:%d} ] {
+  @@ %sparam_reorder.php 31 - 31
+
+  - Parameters [4] {
+    Parameter #0 [ <required> $c ]
+    Parameter #1 [ <required> $b ]
+    Parameter #2 [ <required> $a ]
+    Parameter #3 [ <optional> $d = NULL ]
+  }
+}
+
+array(4) {
+  [0]=>
+  int(3)
+  [1]=>
+  int(2)
+  [2]=>
+  int(1)
+  [3]=>
+  int(4)
+}
+# Some named, some implicit added by '...' on variadic function
+Closure [ <user> static function {closure:%s:%d} ] {
+  @@ %sparam_reorder.php 37 - 37
+
+  - Parameters [4] {
+    Parameter #0 [ <required> $c ]
+    Parameter #1 [ <required> $b ]
+    Parameter #2 [ <required> $a ]
+    Parameter #3 [ <optional> ...$d ]
+  }
+}
+
+array(4) {
+  [0]=>
+  int(3)
+  [1]=>
+  int(2)
+  [2]=>
+  int(1)
+  [3]=>
+  array(3) {
+    [0]=>
+    int(4)
+    [1]=>
+    int(5)
+    [2]=>
+    int(6)
+  }
+}
+# Some prebound, some named
+Closure [ <user> static function {closure:%s:%d} ] {
+  @@ %sparam_reorder.php 43 - 43
+
+  - Bound Variables [2] {
+      Variable #0 [ $a ]
+      Variable #1 [ $d ]
+  }
+
+  - Parameters [2] {
+    Parameter #0 [ <required> $c ]
+    Parameter #1 [ <required> $b ]
+  }
+}
+
+array(4) {
+  [0]=>
+  int(-1)
+  [1]=>
+  int(2)
+  [2]=>
+  int(1)
+  [3]=>
+  int(-2)
+}
+# Some named, some required missing
+ArgumentCountError: f(): Argument #1 ($a) not passed
diff --git a/Zend/tests/partial_application/pipe_optimization_001.phpt b/Zend/tests/partial_application/pipe_optimization_001.phpt
new file mode 100644
index 00000000000..9610f37b76a
--- /dev/null
+++ b/Zend/tests/partial_application/pipe_optimization_001.phpt
@@ -0,0 +1,47 @@
+--TEST--
+PFA optimization: PFA with single placeholder arg can be optimized
+--EXTENSIONS--
+opcache
+--INI--
+opcache.opt_debug_level=0x20000
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_cache=
+opcache.file_cache_only=0
+--FILE--
+<?php
+
+if (time() > 0) {
+    function foo($a) {
+        var_dump($a);
+    }
+}
+
+1 |> foo(?);
+
+?>
+--EXPECTF--
+$_main:
+     ; (lines=9, args=0, vars=0, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_001.php:1-12
+0000 INIT_FCALL 0 %d string("time")
+0001 T1 = DO_ICALL
+0002 T0 = IS_SMALLER int(0) T1
+0003 JMPZ T0 0005
+0004 DECLARE_FUNCTION string("foo") 0
+0005 INIT_FCALL_BY_NAME 1 string("foo")
+0006 SEND_VAL_EX int(1) 1
+0007 DO_FCALL_BY_NAME
+0008 RETURN int(1)
+
+foo:
+     ; (lines=5, args=1, vars=1, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_001.php:4-6
+0000 CV0($a) = RECV 1
+0001 INIT_FCALL 1 %d string("var_dump")
+0002 SEND_VAR CV0($a) 1
+0003 DO_ICALL
+0004 RETURN null
+int(1)
diff --git a/Zend/tests/partial_application/pipe_optimization_002.phpt b/Zend/tests/partial_application/pipe_optimization_002.phpt
new file mode 100644
index 00000000000..729e70d30e6
--- /dev/null
+++ b/Zend/tests/partial_application/pipe_optimization_002.phpt
@@ -0,0 +1,51 @@
+--TEST--
+PFA pipe optimization: PFA with only one placeholder can be optimized
+--EXTENSIONS--
+opcache
+--INI--
+opcache.opt_debug_level=0x20000
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_cache=
+opcache.file_cache_only=0
+--FILE--
+<?php
+
+if (time() > 0) {
+    function foo($a, $b) {
+        var_dump($a, $b);
+    }
+}
+
+2 |> foo(1, ?);
+
+?>
+--EXPECTF--
+$_main:
+     ; (lines=10, args=0, vars=0, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_002.php:1-12
+0000 INIT_FCALL 0 %d string("time")
+0001 T1 = DO_ICALL
+0002 T0 = IS_SMALLER int(0) T1
+0003 JMPZ T0 0005
+0004 DECLARE_FUNCTION string("foo") 0
+0005 INIT_FCALL_BY_NAME 2 string("foo")
+0006 SEND_VAL_EX int(1) 1
+0007 SEND_VAL_EX int(2) 2
+0008 DO_FCALL_BY_NAME
+0009 RETURN int(1)
+
+foo:
+     ; (lines=7, args=2, vars=2, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_002.php:4-6
+0000 CV0($a) = RECV 1
+0001 CV1($b) = RECV 2
+0002 INIT_FCALL 2 %d string("var_dump")
+0003 SEND_VAR CV0($a) 1
+0004 SEND_VAR CV1($b) 2
+0005 DO_ICALL
+0006 RETURN null
+int(1)
+int(2)
diff --git a/Zend/tests/partial_application/pipe_optimization_003.phpt b/Zend/tests/partial_application/pipe_optimization_003.phpt
new file mode 100644
index 00000000000..da112f8f3ce
--- /dev/null
+++ b/Zend/tests/partial_application/pipe_optimization_003.phpt
@@ -0,0 +1,51 @@
+--TEST--
+PFA pipe optimization: PFA with only one placeholder can be optimized (placeholder first)
+--EXTENSIONS--
+opcache
+--INI--
+opcache.opt_debug_level=0x20000
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_cache=
+opcache.file_cache_only=0
+--FILE--
+<?php
+
+if (time() > 0) {
+    function foo($a, $b) {
+        var_dump($a, $b);
+    }
+}
+
+2 |> foo(?, 1);
+
+?>
+--EXPECTF--
+$_main:
+     ; (lines=10, args=0, vars=0, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_003.php:1-12
+0000 INIT_FCALL 0 %d string("time")
+0001 T1 = DO_ICALL
+0002 T0 = IS_SMALLER int(0) T1
+0003 JMPZ T0 0005
+0004 DECLARE_FUNCTION string("foo") 0
+0005 INIT_FCALL_BY_NAME 2 string("foo")
+0006 SEND_VAL_EX int(2) 1
+0007 SEND_VAL_EX int(1) 2
+0008 DO_FCALL_BY_NAME
+0009 RETURN int(1)
+
+foo:
+     ; (lines=7, args=2, vars=2, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_003.php:4-6
+0000 CV0($a) = RECV 1
+0001 CV1($b) = RECV 2
+0002 INIT_FCALL 2 %d string("var_dump")
+0003 SEND_VAR CV0($a) 1
+0004 SEND_VAR CV1($b) 2
+0005 DO_ICALL
+0006 RETURN null
+int(2)
+int(1)
diff --git a/Zend/tests/partial_application/pipe_optimization_004.phpt b/Zend/tests/partial_application/pipe_optimization_004.phpt
new file mode 100644
index 00000000000..addee498d81
--- /dev/null
+++ b/Zend/tests/partial_application/pipe_optimization_004.phpt
@@ -0,0 +1,88 @@
+--TEST--
+PFA pipe optimization: PFA with multiple placeholders can not be optimized
+--EXTENSIONS--
+opcache
+--INI--
+opcache.opt_debug_level=0x20000
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_cache=
+opcache.file_cache_only=0
+--FILE--
+<?php
+
+if (time() > 0) {
+    function foo($a, $b) {
+        var_dump($a, $b);
+    }
+}
+
+try {
+    2 |> foo(?, ?);
+} catch (\Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+$_main:
+     ; (lines=22, args=0, vars=1, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_004.php:1-16
+0000 INIT_FCALL 0 %d string("time")
+0001 T2 = DO_ICALL
+0002 T1 = IS_SMALLER int(0) T2
+0003 JMPZ T1 0005
+0004 DECLARE_FUNCTION string("foo") 0
+0005 INIT_FCALL_BY_NAME 2 string("foo")
+0006 SEND_PLACEHOLDER 1
+0007 SEND_PLACEHOLDER 2
+0008 T1 = CALLABLE_CONVERT_PARTIAL %d
+0009 INIT_DYNAMIC_CALL 1 T1
+0010 SEND_VAL_EX int(2) 1
+0011 DO_FCALL
+0012 RETURN int(1)
+0013 CV0($e) = CATCH string("Throwable")
+0014 T1 = FETCH_CLASS_NAME CV0($e)
+0015 ECHO T1
+0016 ECHO string(": ")
+0017 INIT_METHOD_CALL 0 CV0($e) string("getMessage")
+0018 T1 = DO_FCALL
+0019 ECHO T1
+0020 ECHO string("\n")
+0021 RETURN int(1)
+EXCEPTION TABLE:
+     0005, 0013, -, -
+
+foo:
+     ; (lines=7, args=2, vars=2, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_004.php:4-6
+0000 CV0($a) = RECV 1
+0001 CV1($b) = RECV 2
+0002 INIT_FCALL 2 %d string("var_dump")
+0003 SEND_VAR CV0($a) 1
+0004 SEND_VAR CV1($b) 2
+0005 DO_ICALL
+0006 RETURN null
+
+$_main:
+     ; (lines=3, args=0, vars=0, tmps=%d)
+     ; (after optimizer)
+     ; %s:1-10
+0000 T0 = DECLARE_LAMBDA_FUNCTION 0
+0001 FREE T0
+0002 RETURN int(1)
+
+{closure:%s:%d}:
+     ; (lines=7, args=2, vars=2, tmps=%d)
+     ; (after optimizer)
+     ; %s:10-10
+0000 CV0($a) = RECV 1
+0001 CV1($b) = RECV 2
+0002 INIT_FCALL 2 %d string("foo")
+0003 SEND_VAR CV0($a) 1
+0004 SEND_VAR CV1($b) 2
+0005 T2 = DO_UCALL
+0006 RETURN T2
+ArgumentCountError: Too few arguments to function {closure:pfa:%s:%d}(), 1 passed in %s on line %d and exactly 2 expected
diff --git a/Zend/tests/partial_application/pipe_optimization_005.phpt b/Zend/tests/partial_application/pipe_optimization_005.phpt
new file mode 100644
index 00000000000..3ccfec83660
--- /dev/null
+++ b/Zend/tests/partial_application/pipe_optimization_005.phpt
@@ -0,0 +1,51 @@
+--TEST--
+PFA pipe optimization: PFA with only one placeholder can be optimized (variadic)
+--EXTENSIONS--
+opcache
+--INI--
+opcache.opt_debug_level=0x20000
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_cache=
+opcache.file_cache_only=0
+--FILE--
+<?php
+
+if (time() > 0) {
+    function foo($a, $b) {
+        var_dump($a, $b);
+    }
+}
+
+2 |> foo(1, ...);
+
+?>
+--EXPECTF--
+$_main:
+     ; (lines=10, args=0, vars=0, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_005.php:1-12
+0000 INIT_FCALL 0 %d string("time")
+0001 T1 = DO_ICALL
+0002 T0 = IS_SMALLER int(0) T1
+0003 JMPZ T0 0005
+0004 DECLARE_FUNCTION string("foo") 0
+0005 INIT_FCALL_BY_NAME 2 string("foo")
+0006 SEND_VAL_EX int(1) 1
+0007 SEND_VAL_EX int(2) 2
+0008 DO_FCALL_BY_NAME
+0009 RETURN int(1)
+
+foo:
+     ; (lines=7, args=2, vars=2, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_005.php:4-6
+0000 CV0($a) = RECV 1
+0001 CV1($b) = RECV 2
+0002 INIT_FCALL 2 %d string("var_dump")
+0003 SEND_VAR CV0($a) 1
+0004 SEND_VAR CV1($b) 2
+0005 DO_ICALL
+0006 RETURN null
+int(1)
+int(2)
diff --git a/Zend/tests/partial_application/pipe_optimization_006.phpt b/Zend/tests/partial_application/pipe_optimization_006.phpt
new file mode 100644
index 00000000000..6e06477427a
--- /dev/null
+++ b/Zend/tests/partial_application/pipe_optimization_006.phpt
@@ -0,0 +1,55 @@
+--TEST--
+PFA pipe optimization: PFA with only one placeholder can be optimized (named)
+--EXTENSIONS--
+opcache
+--INI--
+opcache.opt_debug_level=0x20000
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_cache=
+opcache.file_cache_only=0
+--FILE--
+<?php
+
+if (time() > 0) {
+    function foo($a, $b = null, $c = null) {
+        var_dump($a, $b, $c);
+    }
+}
+
+2 |> foo(1, c: ?);
+
+?>
+--EXPECTF--
+$_main:
+     ; (lines=11, args=0, vars=0, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_006.php:1-12
+0000 INIT_FCALL 0 %d string("time")
+0001 T1 = DO_ICALL
+0002 T0 = IS_SMALLER int(0) T1
+0003 JMPZ T0 0005
+0004 DECLARE_FUNCTION string("foo") 0
+0005 INIT_FCALL_BY_NAME 1 string("foo")
+0006 SEND_VAL_EX int(1) 1
+0007 SEND_VAL_EX int(2) string("c")
+0008 CHECK_UNDEF_ARGS
+0009 DO_FCALL_BY_NAME
+0010 RETURN int(1)
+
+foo:
+     ; (lines=9, args=3, vars=3, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_006.php:4-6
+0000 CV0($a) = RECV 1
+0001 CV1($b) = RECV_INIT 2 null
+0002 CV2($c) = RECV_INIT 3 null
+0003 INIT_FCALL 3 %d string("var_dump")
+0004 SEND_VAR CV0($a) 1
+0005 SEND_VAR CV1($b) 2
+0006 SEND_VAR CV2($c) 3
+0007 DO_ICALL
+0008 RETURN null
+int(1)
+NULL
+int(2)
diff --git a/Zend/tests/partial_application/pipe_optimization_007.phpt b/Zend/tests/partial_application/pipe_optimization_007.phpt
new file mode 100644
index 00000000000..09c3c765d23
--- /dev/null
+++ b/Zend/tests/partial_application/pipe_optimization_007.phpt
@@ -0,0 +1,88 @@
+--TEST--
+PFA pipe optimization: PFA with multiple placeholders can not be optimized (named)
+--EXTENSIONS--
+opcache
+--INI--
+opcache.opt_debug_level=0x20000
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_cache=
+opcache.file_cache_only=0
+--FILE--
+<?php
+
+if (time() > 0) {
+    function foo($a, $b) {
+        var_dump($a, $b);
+    }
+}
+
+try {
+    2 |> foo(a: ?, b: ?);
+} catch (\Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+$_main:
+     ; (lines=22, args=0, vars=1, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_007.php:1-16
+0000 INIT_FCALL 0 %d string("time")
+0001 T2 = DO_ICALL
+0002 T1 = IS_SMALLER int(0) T2
+0003 JMPZ T1 0005
+0004 DECLARE_FUNCTION string("foo") 0
+0005 INIT_FCALL_BY_NAME 0 string("foo")
+0006 SEND_PLACEHOLDER string("a")
+0007 SEND_PLACEHOLDER string("b")
+0008 T1 = CALLABLE_CONVERT_PARTIAL %d array(...)
+0009 INIT_DYNAMIC_CALL 1 T1
+0010 SEND_VAL_EX int(2) 1
+0011 DO_FCALL
+0012 RETURN int(1)
+0013 CV0($e) = CATCH string("Throwable")
+0014 T1 = FETCH_CLASS_NAME CV0($e)
+0015 ECHO T1
+0016 ECHO string(": ")
+0017 INIT_METHOD_CALL 0 CV0($e) string("getMessage")
+0018 T1 = DO_FCALL
+0019 ECHO T1
+0020 ECHO string("\n")
+0021 RETURN int(1)
+EXCEPTION TABLE:
+     0005, 0013, -, -
+
+foo:
+     ; (lines=7, args=2, vars=2, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_007.php:4-6
+0000 CV0($a) = RECV 1
+0001 CV1($b) = RECV 2
+0002 INIT_FCALL 2 %d string("var_dump")
+0003 SEND_VAR CV0($a) 1
+0004 SEND_VAR CV1($b) 2
+0005 DO_ICALL
+0006 RETURN null
+
+$_main:
+     ; (lines=3, args=0, vars=0, tmps=%d)
+     ; (after optimizer)
+     ; %s:1-10
+0000 T0 = DECLARE_LAMBDA_FUNCTION 0
+0001 FREE T0
+0002 RETURN int(1)
+
+{closure:%s:%d}:
+     ; (lines=7, args=2, vars=2, tmps=%d)
+     ; (after optimizer)
+     ; %s:10-10
+0000 CV0($a) = RECV 1
+0001 CV1($b) = RECV 2
+0002 INIT_FCALL 2 %d string("foo")
+0003 SEND_VAR CV0($a) 1
+0004 SEND_VAR CV1($b) 2
+0005 T2 = DO_UCALL
+0006 RETURN T2
+ArgumentCountError: Too few arguments to function {closure:pfa:%s:%d}(), 1 passed in %s on line %d and exactly 2 expected
diff --git a/Zend/tests/partial_application/pipe_optimization_008.phpt b/Zend/tests/partial_application/pipe_optimization_008.phpt
new file mode 100644
index 00000000000..070074632c7
--- /dev/null
+++ b/Zend/tests/partial_application/pipe_optimization_008.phpt
@@ -0,0 +1,99 @@
+--TEST--
+PFA pipe optimization: PFA with both a variadic placeholder and named arg can not be optimized
+--EXTENSIONS--
+opcache
+--INI--
+opcache.opt_debug_level=0x20000
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_cache=
+opcache.file_cache_only=0
+--FILE--
+<?php
+
+if (time() > 0) {
+    function foo($a, $b) {
+        var_dump($a, $b);
+    }
+}
+
+try {
+    2 |> foo(a: 1, ...);
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+$_main:
+     ; (lines=18, args=0, vars=1, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_008.php:1-16
+0000 INIT_FCALL 0 %d string("time")
+0001 T2 = DO_ICALL
+0002 T1 = IS_SMALLER int(0) T2
+0003 JMPZ T1 0005
+0004 DECLARE_FUNCTION string("foo") 0
+0005 INIT_FCALL_BY_NAME 0 string("foo")
+0006 SEND_VAL_EX int(1) string("a")
+0007 T1 = CALLABLE_CONVERT_PARTIAL %d
+0008 INIT_DYNAMIC_CALL 1 T1
+0009 SEND_VAL_EX int(2) 1
+0010 DO_FCALL
+0011 RETURN int(1)
+0012 CV0($e) = CATCH string("Throwable")
+0013 INIT_METHOD_CALL 0 CV0($e) string("getMessage")
+0014 T1 = DO_FCALL
+0015 ECHO T1
+0016 ECHO string("\n")
+0017 RETURN int(1)
+EXCEPTION TABLE:
+     0005, 0012, -, -
+
+foo:
+     ; (lines=7, args=2, vars=2, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_008.php:4-6
+0000 CV0($a) = RECV 1
+0001 CV1($b) = RECV 2
+0002 INIT_FCALL 2 %d string("var_dump")
+0003 SEND_VAR CV0($a) 1
+0004 SEND_VAR CV1($b) 2
+0005 DO_ICALL
+0006 RETURN null
+
+$_main:
+     ; (lines=4, args=0, vars=1, tmps=%d)
+     ; (after optimizer)
+     ; %s:1-10
+0000 T1 = DECLARE_LAMBDA_FUNCTION 0
+0001 BIND_LEXICAL T1 CV0($a)
+0002 FREE T1
+0003 RETURN int(1)
+LIVE RANGES:
+     1: 0001 - 0002 (tmp/var)
+
+{closure:%s:%d}:
+     ; (lines=18, args=1, vars=2, tmps=%d)
+     ; (after optimizer)
+     ; %s:10-10
+0000 CV0($b) = RECV 1
+0001 BIND_STATIC CV1($a)
+0002 T3 = FUNC_NUM_ARGS
+0003 T2 = IS_SMALLER_OR_EQUAL T3 int(1)
+0004 JMPZ T2 0010
+0005 INIT_FCALL 2 %d string("foo")
+0006 SEND_VAR CV1($a) 1
+0007 SEND_VAR CV0($b) 2
+0008 T2 = DO_UCALL
+0009 RETURN T2
+0010 INIT_FCALL 2 %d string("foo")
+0011 SEND_VAR CV1($a) 1
+0012 SEND_VAR CV0($b) 2
+0013 T2 = FUNC_GET_ARGS int(1)
+0014 SEND_UNPACK T2
+0015 CHECK_UNDEF_ARGS
+0016 T2 = DO_UCALL
+0017 RETURN T2
+int(1)
+int(2)
diff --git a/Zend/tests/partial_application/pipe_optimization_009.phpt b/Zend/tests/partial_application/pipe_optimization_009.phpt
new file mode 100644
index 00000000000..118482a3860
--- /dev/null
+++ b/Zend/tests/partial_application/pipe_optimization_009.phpt
@@ -0,0 +1,102 @@
+--TEST--
+PFA pipe optimization: Evaluation order
+--EXTENSIONS--
+opcache
+--INI--
+opcache.opt_debug_level=0x20000
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_cache=
+opcache.file_cache_only=0
+--FILE--
+<?php
+
+if (time() > 0) {
+    function foo($a, $b, $c) {
+        var_dump($a, $b, $c);
+    }
+    function lhs() {
+        echo __FUNCTION__, "\n";
+        return 0;
+    }
+    function arg1() {
+        echo __FUNCTION__, "\n";
+        return 1;
+    }
+    function arg2() {
+        echo __FUNCTION__, "\n";
+        return 2;
+    }
+}
+
+lhs() |> foo(arg1(), ?, arg2());
+
+?>
+--EXPECTF--
+$_main:
+     ; (lines=20, args=0, vars=0, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_009.php:1-24
+0000 INIT_FCALL 0 %d string("time")
+0001 T1 = DO_ICALL
+0002 T0 = IS_SMALLER int(0) T1
+0003 JMPZ T0 0008
+0004 DECLARE_FUNCTION string("foo") 0
+0005 DECLARE_FUNCTION string("lhs") 1
+0006 DECLARE_FUNCTION string("arg1") 2
+0007 DECLARE_FUNCTION string("arg2") 3
+0008 INIT_FCALL_BY_NAME 0 string("lhs")
+0009 T0 = DO_FCALL_BY_NAME
+0010 INIT_FCALL_BY_NAME 3 string("foo")
+0011 INIT_FCALL_BY_NAME 0 string("arg1")
+0012 V1 = DO_FCALL_BY_NAME
+0013 SEND_VAR_NO_REF_EX V1 1
+0014 SEND_VAL_EX T0 2
+0015 INIT_FCALL_BY_NAME 0 string("arg2")
+0016 V0 = DO_FCALL_BY_NAME
+0017 SEND_VAR_NO_REF_EX V0 3
+0018 DO_FCALL_BY_NAME
+0019 RETURN int(1)
+LIVE RANGES:
+     0: 0010 - 0014 (tmp/var)
+
+foo:
+     ; (lines=9, args=3, vars=3, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_009.php:4-6
+0000 CV0($a) = RECV 1
+0001 CV1($b) = RECV 2
+0002 CV2($c) = RECV 3
+0003 INIT_FCALL 3 %d string("var_dump")
+0004 SEND_VAR CV0($a) 1
+0005 SEND_VAR CV1($b) 2
+0006 SEND_VAR CV2($c) 3
+0007 DO_ICALL
+0008 RETURN null
+
+lhs:
+     ; (lines=2, args=0, vars=0, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_009.php:7-10
+0000 ECHO string("lhs\n")
+0001 RETURN int(0)
+
+arg1:
+     ; (lines=2, args=0, vars=0, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_009.php:11-14
+0000 ECHO string("arg1\n")
+0001 RETURN int(1)
+
+arg2:
+     ; (lines=2, args=0, vars=0, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_009.php:15-18
+0000 ECHO string("arg2\n")
+0001 RETURN int(2)
+lhs
+arg1
+arg2
+int(1)
+int(0)
+int(2)
diff --git a/Zend/tests/partial_application/pipe_optimization_010.phpt b/Zend/tests/partial_application/pipe_optimization_010.phpt
new file mode 100644
index 00000000000..f77c2a97732
--- /dev/null
+++ b/Zend/tests/partial_application/pipe_optimization_010.phpt
@@ -0,0 +1,58 @@
+--TEST--
+PFA pipe optimization: References
+--EXTENSIONS--
+opcache
+--INI--
+opcache.opt_debug_level=0x20000
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_cache=
+opcache.file_cache_only=0
+--FILE--
+<?php
+
+if (time() > 0) {
+    function foo(&$a, $b) {
+        var_dump($a, $b);
+        $a = 2;
+    }
+}
+
+1 |> foo($a, ?);
+var_dump($a);
+
+?>
+--EXPECTF--
+$_main:
+     ; (lines=13, args=0, vars=1, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_010.php:1-14
+0000 INIT_FCALL 0 %d string("time")
+0001 T2 = DO_ICALL
+0002 T1 = IS_SMALLER int(0) T2
+0003 JMPZ T1 0005
+0004 DECLARE_FUNCTION string("foo") 0
+0005 INIT_FCALL_BY_NAME 2 string("foo")
+0006 SEND_VAR_EX CV0($a) 1
+0007 SEND_VAL_EX int(1) 2
+0008 DO_FCALL_BY_NAME
+0009 INIT_FCALL 1 %d string("var_dump")
+0010 SEND_VAR CV0($a) 1
+0011 DO_ICALL
+0012 RETURN int(1)
+
+foo:
+     ; (lines=8, args=2, vars=2, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_010.php:4-7
+0000 CV0($a) = RECV 1
+0001 CV1($b) = RECV 2
+0002 INIT_FCALL 2 %d string("var_dump")
+0003 SEND_VAR CV0($a) 1
+0004 SEND_VAR CV1($b) 2
+0005 DO_ICALL
+0006 ASSIGN CV0($a) int(2)
+0007 RETURN null
+NULL
+int(1)
+int(2)
diff --git a/Zend/tests/partial_application/pipe_optimization_011.phpt b/Zend/tests/partial_application/pipe_optimization_011.phpt
new file mode 100644
index 00000000000..cd1c986d99e
--- /dev/null
+++ b/Zend/tests/partial_application/pipe_optimization_011.phpt
@@ -0,0 +1,98 @@
+--TEST--
+PFA pipe optimization: Evaluation order
+--EXTENSIONS--
+opcache
+--INI--
+opcache.opt_debug_level=0x20000
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_cache=
+opcache.file_cache_only=0
+--FILE--
+<?php
+
+if (time() > 0) {
+    function foo($a, $b, $c) {
+        var_dump($a, $b, $c);
+    }
+    function arg1() {
+        global $a;
+        $a = 2;
+        echo __FUNCTION__, "\n";
+        return 1;
+    }
+    function arg2() {
+        global $a;
+        $a = 3;
+        echo __FUNCTION__, "\n";
+        return 2;
+    }
+}
+
+$a = 0;
+$a |> foo(arg1(), ?, arg2());
+
+?>
+--EXPECTF--
+$_main:
+     ; (lines=19, args=0, vars=1, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_011.php:1-25
+0000 INIT_FCALL 0 %d string("time")
+0001 T2 = DO_ICALL
+0002 T1 = IS_SMALLER int(0) T2
+0003 JMPZ T1 0007
+0004 DECLARE_FUNCTION string("foo") 0
+0005 DECLARE_FUNCTION string("arg1") %d
+0006 DECLARE_FUNCTION string("arg2") %d
+0007 ASSIGN CV0($a) int(0)
+0008 T1 = QM_ASSIGN CV0($a)
+0009 INIT_FCALL_BY_NAME 3 string("foo")
+0010 INIT_FCALL_BY_NAME 0 string("arg1")
+0011 V2 = DO_FCALL_BY_NAME
+0012 SEND_VAR_NO_REF_EX V2 1
+0013 SEND_VAL_EX T1 2
+0014 INIT_FCALL_BY_NAME 0 string("arg2")
+0015 V1 = DO_FCALL_BY_NAME
+0016 SEND_VAR_NO_REF_EX V1 3
+0017 DO_FCALL_BY_NAME
+0018 RETURN int(1)
+LIVE RANGES:
+     1: 0009 - 0013 (tmp/var)
+
+foo:
+     ; (lines=9, args=3, vars=3, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_011.php:4-6
+0000 CV0($a) = RECV 1
+0001 CV1($b) = RECV 2
+0002 CV2($c) = RECV 3
+0003 INIT_FCALL 3 %d string("var_dump")
+0004 SEND_VAR CV0($a) 1
+0005 SEND_VAR CV1($b) 2
+0006 SEND_VAR CV2($c) 3
+0007 DO_ICALL
+0008 RETURN null
+
+arg1:
+     ; (lines=4, args=0, vars=1, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_011.php:7-12
+0000 BIND_GLOBAL CV0($a) string("a")
+0001 ASSIGN CV0($a) int(2)
+0002 ECHO string("arg1\n")
+0003 RETURN int(1)
+
+arg2:
+     ; (lines=4, args=0, vars=1, tmps=%d)
+     ; (after optimizer)
+     ; %spipe_optimization_011.php:13-18
+0000 BIND_GLOBAL CV0($a) string("a")
+0001 ASSIGN CV0($a) int(3)
+0002 ECHO string("arg2\n")
+0003 RETURN int(2)
+arg1
+arg2
+int(1)
+int(0)
+int(2)
diff --git a/Zend/tests/partial_application/pipe_optimization_012.phpt b/Zend/tests/partial_application/pipe_optimization_012.phpt
new file mode 100644
index 00000000000..da172874adc
--- /dev/null
+++ b/Zend/tests/partial_application/pipe_optimization_012.phpt
@@ -0,0 +1,52 @@
+--TEST--
+PFA optimization: PFA with named args and placeholders can be optimized
+--EXTENSIONS--
+opcache
+--INI--
+opcache.opt_debug_level=0x20000
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_cache=
+opcache.file_cache_only=0
+--FILE--
+<?php
+
+if (time() > 0) {
+    function foo($a, $b) {
+        var_dump($a, $b);
+    }
+}
+
+1 |> foo(?, b: 2);
+
+?>
+--EXPECTF--
+$_main:
+     ; (lines=11, args=0, vars=0, tmps=%d)
+     ; (after optimizer)
+     ; %s:1-12
+0000 INIT_FCALL 0 %d string("time")
+0001 T1 = DO_ICALL
+0002 T0 = IS_SMALLER int(0) T1
+0003 JMPZ T0 0005
+0004 DECLARE_FUNCTION string("foo") 0
+0005 INIT_FCALL_BY_NAME 1 string("foo")
+0006 SEND_VAL_EX int(1) 1
+0007 SEND_VAL_EX int(2) string("b")
+0008 CHECK_UNDEF_ARGS
+0009 DO_FCALL_BY_NAME
+0010 RETURN int(1)
+
+foo:
+     ; (lines=7, args=2, vars=2, tmps=%d)
+     ; (after optimizer)
+     ; %s:4-6
+0000 CV0($a) = RECV 1
+0001 CV1($b) = RECV 2
+0002 INIT_FCALL 2 %d string("var_dump")
+0003 SEND_VAR CV0($a) 1
+0004 SEND_VAR CV1($b) 2
+0005 DO_ICALL
+0006 RETURN null
+int(1)
+int(2)
diff --git a/Zend/tests/partial_application/pipe_optimization_013.phpt b/Zend/tests/partial_application/pipe_optimization_013.phpt
new file mode 100644
index 00000000000..7d1a48b2f2e
--- /dev/null
+++ b/Zend/tests/partial_application/pipe_optimization_013.phpt
@@ -0,0 +1,87 @@
+--TEST--
+PFA optimization: PFA with named args and a variadic placeholder can not be optimized
+--EXTENSIONS--
+opcache
+--INI--
+opcache.opt_debug_level=0x20000
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_cache=
+opcache.file_cache_only=0
+--FILE--
+<?php
+
+if (time() > 0) {
+    function foo($a, $b) {
+        var_dump($a, $b);
+    }
+}
+
+1 |> foo(b: 2, ...);
+
+?>
+--EXPECTF--
+$_main:
+     ; (lines=12, args=0, vars=0, tmps=%d)
+     ; (after optimizer)
+     ; %s:1-12
+0000 INIT_FCALL 0 %d string("time")
+0001 T1 = DO_ICALL
+0002 T0 = IS_SMALLER int(0) T1
+0003 JMPZ T0 0005
+0004 DECLARE_FUNCTION string("foo") 0
+0005 INIT_FCALL_BY_NAME 0 string("foo")
+0006 SEND_VAL_EX int(2) string("b")
+0007 T0 = CALLABLE_CONVERT_PARTIAL 3
+0008 INIT_DYNAMIC_CALL 1 T0
+0009 SEND_VAL_EX int(1) 1
+0010 DO_FCALL
+0011 RETURN int(1)
+
+foo:
+     ; (lines=7, args=2, vars=2, tmps=%d)
+     ; (after optimizer)
+     ; %s:4-6
+0000 CV0($a) = RECV 1
+0001 CV1($b) = RECV 2
+0002 INIT_FCALL 2 %d string("var_dump")
+0003 SEND_VAR CV0($a) 1
+0004 SEND_VAR CV1($b) 2
+0005 DO_ICALL
+0006 RETURN null
+
+$_main:
+     ; (lines=4, args=0, vars=1, tmps=%d)
+     ; (after optimizer)
+     ; %s:1-9
+0000 T1 = DECLARE_LAMBDA_FUNCTION 0
+0001 BIND_LEXICAL T1 CV0($b)
+0002 FREE T1
+0003 RETURN int(1)
+LIVE RANGES:
+     1: 0001 - 0002 (tmp/var)
+
+{closure:pfa:%s:9}:
+     ; (lines=18, args=1, vars=2, tmps=%d)
+     ; (after optimizer)
+     ; %s:9-9
+0000 CV0($a) = RECV 1
+0001 BIND_STATIC CV1($b)
+0002 T3 = FUNC_NUM_ARGS
+0003 T2 = IS_SMALLER_OR_EQUAL T3 int(1)
+0004 JMPZ T2 0010
+0005 INIT_FCALL 2 %d string("foo")
+0006 SEND_VAR CV0($a) 1
+0007 SEND_VAR CV1($b) 2
+0008 T2 = DO_UCALL
+0009 RETURN T2
+0010 INIT_FCALL 2 %d string("foo")
+0011 SEND_VAR CV0($a) 1
+0012 SEND_VAR CV1($b) 2
+0013 T2 = FUNC_GET_ARGS int(1)
+0014 SEND_UNPACK T2
+0015 CHECK_UNDEF_ARGS
+0016 T2 = DO_UCALL
+0017 RETURN T2
+int(1)
+int(2)
diff --git a/Zend/tests/partial_application/pipe_optimization_014.phpt b/Zend/tests/partial_application/pipe_optimization_014.phpt
new file mode 100644
index 00000000000..6e66d7e0e99
--- /dev/null
+++ b/Zend/tests/partial_application/pipe_optimization_014.phpt
@@ -0,0 +1,68 @@
+--TEST--
+PFA pipe optimization: PFA with unknown named parameter can be optimized
+--EXTENSIONS--
+opcache
+--INI--
+opcache.opt_debug_level=0x20000
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_cache=
+opcache.file_cache_only=0
+--FILE--
+<?php
+
+if (time() > 0) {
+    function foo($a, $b = null, $c = null) {
+        var_dump($a, $b, $c);
+    }
+}
+
+try {
+    2 |> foo(1, unknown: ?);
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+$_main:
+     ; (lines=20, args=0, vars=1, tmps=%d)
+     ; (after optimizer)
+     ; %s:1-16
+0000 INIT_FCALL 0 %d string("time")
+0001 T2 = DO_ICALL
+0002 T1 = IS_SMALLER int(0) T2
+0003 JMPZ T1 0005
+0004 DECLARE_FUNCTION string("foo") 0
+0005 INIT_FCALL_BY_NAME 1 string("foo")
+0006 SEND_VAL_EX int(1) 1
+0007 SEND_VAL_EX int(2) string("unknown")
+0008 CHECK_UNDEF_ARGS
+0009 DO_FCALL_BY_NAME
+0010 RETURN int(1)
+0011 CV0($e) = CATCH string("Error")
+0012 T1 = FETCH_CLASS_NAME CV0($e)
+0013 ECHO T1
+0014 ECHO string(": ")
+0015 INIT_METHOD_CALL 0 CV0($e) string("getMessage")
+0016 T1 = DO_FCALL
+0017 ECHO T1
+0018 ECHO string("\n")
+0019 RETURN int(1)
+EXCEPTION TABLE:
+     0005, 0011, -, -
+
+foo:
+     ; (lines=9, args=3, vars=3, tmps=%d)
+     ; (after optimizer)
+     ; %s:4-6
+0000 CV0($a) = RECV 1
+0001 CV1($b) = RECV_INIT 2 null
+0002 CV2($c) = RECV_INIT 3 null
+0003 INIT_FCALL 3 %d string("var_dump")
+0004 SEND_VAR CV0($a) 1
+0005 SEND_VAR CV1($b) 2
+0006 SEND_VAR CV2($c) 3
+0007 DO_ICALL
+0008 RETURN null
+Error: Unknown named parameter $unknown
diff --git a/Zend/tests/partial_application/pipe_optimization_015.phpt b/Zend/tests/partial_application/pipe_optimization_015.phpt
new file mode 100644
index 00000000000..ce293c7a300
--- /dev/null
+++ b/Zend/tests/partial_application/pipe_optimization_015.phpt
@@ -0,0 +1,55 @@
+--TEST--
+PFA pipe optimization: PFA with skipped optional parameter can be optimized
+--EXTENSIONS--
+opcache
+--INI--
+opcache.opt_debug_level=0x20000
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_cache=
+opcache.file_cache_only=0
+--FILE--
+<?php
+
+if (time() > 0) {
+    function foo($a, $b = null, $c = null) {
+        var_dump($a, $b, $c);
+    }
+}
+
+3 |> foo(1, c: ?);
+
+?>
+--EXPECTF--
+$_main:
+     ; (lines=11, args=0, vars=0, tmps=%d)
+     ; (after optimizer)
+     ; %s:1-12
+0000 INIT_FCALL 0 %d string("time")
+0001 T1 = DO_ICALL
+0002 T0 = IS_SMALLER int(0) T1
+0003 JMPZ T0 0005
+0004 DECLARE_FUNCTION string("foo") 0
+0005 INIT_FCALL_BY_NAME 1 string("foo")
+0006 SEND_VAL_EX int(1) 1
+0007 SEND_VAL_EX int(3) string("c")
+0008 CHECK_UNDEF_ARGS
+0009 DO_FCALL_BY_NAME
+0010 RETURN int(1)
+
+foo:
+     ; (lines=9, args=3, vars=3, tmps=%d)
+     ; (after optimizer)
+     ; %s:4-6
+0000 CV0($a) = RECV 1
+0001 CV1($b) = RECV_INIT 2 null
+0002 CV2($c) = RECV_INIT 3 null
+0003 INIT_FCALL 3 %d string("var_dump")
+0004 SEND_VAR CV0($a) 1
+0005 SEND_VAR CV1($b) 2
+0006 SEND_VAR CV2($c) 3
+0007 DO_ICALL
+0008 RETURN null
+int(1)
+NULL
+int(3)
diff --git a/Zend/tests/partial_application/preloading.inc b/Zend/tests/partial_application/preloading.inc
new file mode 100644
index 00000000000..885ed0c5b0f
--- /dev/null
+++ b/Zend/tests/partial_application/preloading.inc
@@ -0,0 +1,13 @@
+<?php
+
+function f($abc) {
+    return $abc + 1;
+}
+
+function test() {
+    return f(?);
+}
+
+var_dump(test()(1));
+
+?>
diff --git a/Zend/tests/partial_application/preloading.phpt b/Zend/tests/partial_application/preloading.phpt
new file mode 100644
index 00000000000..23ad6edf2c4
--- /dev/null
+++ b/Zend/tests/partial_application/preloading.phpt
@@ -0,0 +1,19 @@
+--TEST--
+PFA preloading
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.preload={PWD}/preloading.inc
+--SKIPIF--
+<?php
+if (PHP_OS_FAMILY == 'Windows') die('skip Preloading is not supported on Windows');
+?>
+--FILE--
+<?php
+
+var_dump(test()(1));
+
+?>
+--EXPECT--
+int(2)
+int(2)
diff --git a/Zend/tests/partial_application/rebinding_001.phpt b/Zend/tests/partial_application/rebinding_001.phpt
new file mode 100644
index 00000000000..d1913957a8c
--- /dev/null
+++ b/Zend/tests/partial_application/rebinding_001.phpt
@@ -0,0 +1,59 @@
+--TEST--
+PFA can only be rebound to an instanceof $this
+--FILE--
+<?php
+
+class C {
+    function f($a) { var_dump($this); }
+    static function g($a) { var_dump(static::class); }
+}
+
+class SubClass extends C {}
+
+class Unrelated {}
+
+$c = new C;
+$f = $c->f(?);
+$g = $c->g(?);
+
+echo "# Can be rebound to \$this of the same class:\n";
+$f->bindTo(new C)(1);
+
+echo "# Can be rebound to \$this of a sub-class:\n";
+$f->bindTo(new SubClass)(1);
+
+echo "# Cannot be rebound to an unrelated class:\n";
+try {
+    $f->bindTo(new Unrelated)(1);
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+echo "# Cannot unbind \$this on instance method:\n";
+try {
+    $f->bindTo(null)(1);
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+echo "# Can unbind \$this on static method:\n";
+$g->bindTo(null)(1);
+
+?>
+--EXPECTF--
+# Can be rebound to $this of the same class:
+object(C)#%d (0) {
+}
+# Can be rebound to $this of a sub-class:
+object(SubClass)#%d (0) {
+}
+# Cannot be rebound to an unrelated class:
+
+Warning: Cannot bind method C::{closure:%s:%d}() to object of class Unrelated, this will be an error in PHP 9 in %s on line %d
+Error: Value of type null is not callable
+# Cannot unbind $this on instance method:
+
+Warning: Cannot unbind $this of method, this will be an error in PHP 9 in %s on line %d
+Error: Value of type null is not callable
+# Can unbind $this on static method:
+string(1) "C"
diff --git a/Zend/tests/partial_application/rebinding_002.phpt b/Zend/tests/partial_application/rebinding_002.phpt
new file mode 100644
index 00000000000..cfead54bbd8
--- /dev/null
+++ b/Zend/tests/partial_application/rebinding_002.phpt
@@ -0,0 +1,45 @@
+--TEST--
+PFA scope cannot be rebound
+--FILE--
+<?php
+
+class C {
+    function f($a) { var_dump(static::class); }
+}
+
+class SubClass extends C {}
+
+function g($a) { var_dump(__FUNCTION__); }
+
+$c = new C;
+$f = $c->f(?);
+$g = g(?);
+
+echo "# Can be rebound to the same scope:\n";
+$f->bindTo($c, C::class)(1);
+
+echo "# Method cannot be rebound to a different scope:\n";
+try {
+    $f->bindTo($c, SubClass::class)(1);
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+echo "# Function cannot be rebound to a different scope:\n";
+try {
+    $g->bindTo($c, SubClass::class)(1);
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+# Can be rebound to the same scope:
+string(1) "C"
+# Method cannot be rebound to a different scope:
+
+Warning: Cannot rebind scope of closure created from method, this will be an error in PHP 9 in %s on line %d
+Error: Value of type null is not callable
+# Function cannot be rebound to a different scope:
+
+Warning: Cannot bind an instance to a static closure, this will be an error in PHP 9 in %s on line %d
+Error: Value of type null is not callable
diff --git a/Zend/tests/partial_application/rebinding_003.phpt b/Zend/tests/partial_application/rebinding_003.phpt
new file mode 100644
index 00000000000..9417bbc2d32
--- /dev/null
+++ b/Zend/tests/partial_application/rebinding_003.phpt
@@ -0,0 +1,69 @@
+--TEST--
+Rebinding PFA of Closure rebinds inner Closure
+--FILE--
+<?php
+
+class C {
+    function getF() {
+        return function ($a) { var_dump($this, $this === $a); };
+    }
+    function getG() {
+        return static function ($a) { var_dump(static::class, $a); };
+    }
+}
+
+class SubClass extends C {}
+
+class Unrelated {}
+
+$c = new C();
+$f = $c->getF()(?);
+$g = $c->getG()(?);
+
+echo "# Can be rebound to \$this of the same class:\n";
+$d = new C();
+$f->bindTo($d)($d);
+
+echo "# Can be rebound to \$this of a sub-class:\n";
+$d = new SubClass();
+$f->bindTo($d)($d);
+
+echo "# Cannot be rebound to an unrelated class:\n";
+try {
+    $f->bindTo(new Unrelated)($c);
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+echo "# Cannot unbind \$this on non-static closure:\n";
+try {
+    $f->bindTo(null)($c);
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+echo "# Can unbind \$this on static closure:\n";
+$g->bindTo(null)($c);
+
+?>
+--EXPECTF--
+# Can be rebound to $this of the same class:
+object(C)#%d (0) {
+}
+bool(true)
+# Can be rebound to $this of a sub-class:
+object(SubClass)#%d (0) {
+}
+bool(true)
+# Cannot be rebound to an unrelated class:
+
+Warning: Cannot bind method C::{closure:%s:%d}() to object of class Unrelated, this will be an error in PHP 9 in %s on line %d
+Error: Value of type null is not callable
+# Cannot unbind $this on non-static closure:
+
+Warning: Cannot unbind $this of method, this will be an error in PHP 9 in %s on line %d
+Error: Value of type null is not callable
+# Can unbind $this on static closure:
+string(1) "C"
+object(C)#%d (0) {
+}
diff --git a/Zend/tests/partial_application/recorded_warnings.phpt b/Zend/tests/partial_application/recorded_warnings.phpt
new file mode 100644
index 00000000000..f25be826f52
--- /dev/null
+++ b/Zend/tests/partial_application/recorded_warnings.phpt
@@ -0,0 +1,14 @@
+--TEST--
+PFA compilation warnings are recorded and replayed
+--FILE--
+<?php
+
+function f(_ $a) {}
+
+$f = f(?);
+
+?>
+--EXPECTF--
+Deprecated: Using "_" as a type name is deprecated since 8.4 in %s on line 3
+
+Deprecated: Using "_" as a type name is deprecated since 8.4 in %s on line 5
diff --git a/Zend/tests/partial_application/references_001.phpt b/Zend/tests/partial_application/references_001.phpt
new file mode 100644
index 00000000000..080b3085acc
--- /dev/null
+++ b/Zend/tests/partial_application/references_001.phpt
@@ -0,0 +1,24 @@
+--TEST--
+PFA receives by val if the actual function does
+--FILE--
+<?php
+
+function foo($a, $b) {
+    $b = 2;
+}
+
+$a = ['unchanged because foo() doesn\'t take by reference'];
+$b = &$a[0];
+
+$foo = foo(1, ?);
+$foo($b);
+
+var_dump($a, $b);
+
+?>
+--EXPECT--
+array(1) {
+  [0]=>
+  &string(49) "unchanged because foo() doesn't take by reference"
+}
+string(49) "unchanged because foo() doesn't take by reference"
diff --git a/Zend/tests/partial_application/references_002.phpt b/Zend/tests/partial_application/references_002.phpt
new file mode 100644
index 00000000000..42a90648c1a
--- /dev/null
+++ b/Zend/tests/partial_application/references_002.phpt
@@ -0,0 +1,24 @@
+--TEST--
+PFA receives by ref if the actual function does
+--FILE--
+<?php
+
+function foo($a, &$b) {
+    $b = 2;
+}
+
+$a = ['this will be changed'];
+$b = &$a[0];
+
+$foo = foo(1, ?);
+$foo($b);
+
+var_dump($a, $b);
+
+?>
+--EXPECT--
+array(1) {
+  [0]=>
+  &int(2)
+}
+int(2)
diff --git a/Zend/tests/partial_application/references_003.phpt b/Zend/tests/partial_application/references_003.phpt
new file mode 100644
index 00000000000..be116b06c79
--- /dev/null
+++ b/Zend/tests/partial_application/references_003.phpt
@@ -0,0 +1,20 @@
+--TEST--
+PFA receives by ref if the actual function does
+--FILE--
+<?php
+
+function foo($a, &$b) {
+    $a = 2;
+}
+
+$foo = foo(1, ?);
+
+try {
+    $foo(2);
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+{closure:%s}(): Argument #1 ($b) could not be passed by reference
diff --git a/Zend/tests/partial_application/references_004.phpt b/Zend/tests/partial_application/references_004.phpt
new file mode 100644
index 00000000000..e5163c710da
--- /dev/null
+++ b/Zend/tests/partial_application/references_004.phpt
@@ -0,0 +1,42 @@
+--TEST--
+PFA receives variadic param by ref if the actual function does
+--FILE--
+<?php
+
+function foo($a, &...$args) {
+    foreach ($args as &$arg) {
+        $arg = -$arg;
+    }
+}
+
+$b = 2;
+$c = 3;
+$d = 4;
+
+$foo = foo(1, $b, ?, ...);
+
+echo (string) new ReflectionFunction($foo), "\n";
+
+$foo($c, $d);
+
+var_dump($b, $c, $d);
+
+?>
+--EXPECTF--
+Closure [ <user> static function {closure:%s:%d} ] {
+  @@ %sreferences_004.php 13 - 13
+
+  - Bound Variables [2] {
+      Variable #0 [ $a ]
+      Variable #1 [ $args2 ]
+  }
+
+  - Parameters [2] {
+    Parameter #0 [ <required> &$args0 ]
+    Parameter #1 [ <optional> &...$args ]
+  }
+}
+
+int(-2)
+int(-3)
+int(-4)
diff --git a/Zend/tests/partial_application/references_005.phpt b/Zend/tests/partial_application/references_005.phpt
new file mode 100644
index 00000000000..e8c7c27a07b
--- /dev/null
+++ b/Zend/tests/partial_application/references_005.phpt
@@ -0,0 +1,26 @@
+--TEST--
+PFA inherits return by ref
+--FILE--
+<?php
+
+function &foo(&$a) {
+    return $a;
+}
+
+$f = foo(?);
+$a = [];
+$b = &$f($a);
+$a[0] = 1;
+
+var_dump($a, $b);
+
+?>
+--EXPECT--
+array(1) {
+  [0]=>
+  int(1)
+}
+array(1) {
+  [0]=>
+  int(1)
+}
diff --git a/Zend/tests/partial_application/reflection_001.phpt b/Zend/tests/partial_application/reflection_001.phpt
new file mode 100644
index 00000000000..ac079515d0a
--- /dev/null
+++ b/Zend/tests/partial_application/reflection_001.phpt
@@ -0,0 +1,48 @@
+--TEST--
+PFA reflection: optional parameters
+--FILE--
+<?php
+function foo($a = 1, $b = 5, $c = 10) {
+
+}
+
+$foo = foo(?, ...);
+
+echo (string) new ReflectionFunction($foo);
+
+$foo = foo(?, ?, ...);
+
+echo (string) new ReflectionFunction($foo);
+
+$foo = foo(?, ?, ?);
+
+echo (string) new ReflectionFunction($foo);
+?>
+--EXPECTF--
+Closure [ <user> static function {closure:%s:%d} ] {
+  @@ %sreflection_001.php 6 - 6
+
+  - Parameters [3] {
+    Parameter #0 [ <required> $a ]
+    Parameter #1 [ <optional> $b = 5 ]
+    Parameter #2 [ <optional> $c = 10 ]
+  }
+}
+Closure [ <user> static function {closure:%s:%d} ] {
+  @@ %sreflection_001.php 10 - 10
+
+  - Parameters [3] {
+    Parameter #0 [ <required> $a ]
+    Parameter #1 [ <required> $b ]
+    Parameter #2 [ <optional> $c = 10 ]
+  }
+}
+Closure [ <user> static function {closure:%s:%d} ] {
+  @@ %sreflection_001.php 14 - 14
+
+  - Parameters [3] {
+    Parameter #0 [ <required> $a ]
+    Parameter #1 [ <required> $b ]
+    Parameter #2 [ <required> $c ]
+  }
+}
diff --git a/Zend/tests/partial_application/reflection_002.phpt b/Zend/tests/partial_application/reflection_002.phpt
new file mode 100644
index 00000000000..da91a7af50c
--- /dev/null
+++ b/Zend/tests/partial_application/reflection_002.phpt
@@ -0,0 +1,57 @@
+--TEST--
+PFA reflection: variadics
+--FILE--
+<?php
+function foo($a, ...$b) {
+    var_dump(func_get_args());
+}
+
+$foo = foo(?);
+
+echo (string) new ReflectionFunction($foo);
+
+$foo = foo(?, ...);
+
+echo (string) new ReflectionFunction($foo);
+
+$foo = foo(?, ?);
+
+echo (string) new ReflectionFunction($foo);
+
+$foo = foo(?, ?, ?);
+
+echo (string) new ReflectionFunction($foo);
+?>
+--EXPECTF--
+Closure [ <user> static function {closure:%s:%d} ] {
+  @@ %s 6 - 6
+
+  - Parameters [1] {
+    Parameter #0 [ <required> $a ]
+  }
+}
+Closure [ <user> static function {closure:%s:%d} ] {
+  @@ %s 10 - 10
+
+  - Parameters [2] {
+    Parameter #0 [ <required> $a ]
+    Parameter #1 [ <optional> ...$b ]
+  }
+}
+Closure [ <user> static function {closure:%s:%d} ] {
+  @@ %s 14 - 14
+
+  - Parameters [2] {
+    Parameter #0 [ <required> $a ]
+    Parameter #1 [ <required> $b0 ]
+  }
+}
+Closure [ <user> static function {closure:%s:%d} ] {
+  @@ %s 18 - 18
+
+  - Parameters [3] {
+    Parameter #0 [ <required> $a ]
+    Parameter #1 [ <required> $b0 ]
+    Parameter #2 [ <required> $b1 ]
+  }
+}
diff --git a/Zend/tests/partial_application/reflection_003.phpt b/Zend/tests/partial_application/reflection_003.phpt
new file mode 100644
index 00000000000..90506d38a77
--- /dev/null
+++ b/Zend/tests/partial_application/reflection_003.phpt
@@ -0,0 +1,43 @@
+--TEST--
+PFA reflection: internal with variadics
+--FILE--
+<?php
+$foo = sprintf(?);
+
+echo (string) new ReflectionFunction($foo);
+
+$foo = sprintf(?, ...);
+
+echo (string) new ReflectionFunction($foo);
+
+$foo = sprintf(?, ?);
+
+echo (string) new ReflectionFunction($foo);
+?>
+--EXPECTF--
+Closure [ <user> static function {closure:%s:%d} ] {
+  @@ %sreflection_003.php 2 - 2
+
+  - Parameters [1] {
+    Parameter #0 [ <required> string $format ]
+  }
+  - Return [ string ]
+}
+Closure [ <user> static function {closure:%s:%d} ] {
+  @@ %sreflection_003.php 6 - 6
+
+  - Parameters [2] {
+    Parameter #0 [ <required> string $format ]
+    Parameter #1 [ <optional> mixed ...$values ]
+  }
+  - Return [ string ]
+}
+Closure [ <user> static function {closure:%s:%d} ] {
+  @@ %sreflection_003.php 10 - 10
+
+  - Parameters [2] {
+    Parameter #0 [ <required> string $format ]
+    Parameter #1 [ <required> mixed $values0 ]
+  }
+  - Return [ string ]
+}
diff --git a/Zend/tests/partial_application/reflection_004.phpt b/Zend/tests/partial_application/reflection_004.phpt
new file mode 100644
index 00000000000..7226383f5af
--- /dev/null
+++ b/Zend/tests/partial_application/reflection_004.phpt
@@ -0,0 +1,16 @@
+--TEST--
+PFA reflection: ReflectionFunction::isAnonymous() is true for partials
+--FILE--
+<?php
+
+var_dump((new ReflectionFunction('sprintf'))->isAnonymous());
+
+var_dump((new ReflectionFunction(function () {}))->isAnonymous());
+
+var_dump((new ReflectionFunction(sprintf(?)))->isAnonymous());
+
+?>
+--EXPECT--
+bool(false)
+bool(true)
+bool(true)
diff --git a/Zend/tests/partial_application/reflection_005.phpt b/Zend/tests/partial_application/reflection_005.phpt
new file mode 100644
index 00000000000..be86270c004
--- /dev/null
+++ b/Zend/tests/partial_application/reflection_005.phpt
@@ -0,0 +1,16 @@
+--TEST--
+PFA reflection: ReflectionFunction::isClosure() is true for partials
+--FILE--
+<?php
+
+var_dump((new ReflectionFunction('sprintf'))->isClosure());
+
+var_dump((new ReflectionFunction(function () {}))->isClosure());
+
+var_dump((new ReflectionFunction(sprintf(?)))->isClosure());
+
+?>
+--EXPECT--
+bool(false)
+bool(true)
+bool(true)
diff --git a/Zend/tests/partial_application/relative_return_types.phpt b/Zend/tests/partial_application/relative_return_types.phpt
new file mode 100644
index 00000000000..5729f7edfd0
--- /dev/null
+++ b/Zend/tests/partial_application/relative_return_types.phpt
@@ -0,0 +1,148 @@
+--TEST--
+PFA supports relative return types
+--FILE--
+<?php
+
+if (time() > 0) {
+    trait T {
+        public function getSelf(object $o): self {
+            return $o;
+        }
+        public function getStatic(object $o): static {
+            return $o;
+        }
+    }
+}
+
+class C {
+    use T;
+}
+
+class D extends C {
+}
+
+$c = new C;
+
+echo "# C::getSelf():\n";
+
+$self = $c->getSelf(?);
+
+echo (string) new ReflectionFunction($self), "\n";
+
+var_dump($self($c));
+var_dump($self(new D));
+try {
+    $self(new stdClass);
+} catch (Error $e) {
+    echo $e->getMessage(), "\n";
+}
+
+echo "# C::getStatic():\n";
+
+$static = $c->getStatic(?);
+
+echo (string) new ReflectionFunction($static), "\n";
+
+var_dump($static($c));
+var_dump($static(new D));
+try {
+    $static(new stdClass);
+} catch (Error $e) {
+    echo $e->getMessage(), "\n";
+}
+
+$d = new D;
+
+echo "# D::getSelf():\n";
+
+$self = $d->getSelf(?);
+
+echo (string) new ReflectionFunction($self), "\n";
+
+var_dump($self($d));
+var_dump($self(new C));
+try {
+    $self(new stdClass);
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+echo "# D::getStatic():\n";
+
+$static = $d->getStatic(?);
+
+echo (string) new ReflectionFunction($static), "\n";
+
+var_dump($static($d));
+try {
+    var_dump($static(new C));
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+try {
+    $static(new stdClass);
+} catch (Error $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+# C::getSelf():
+Closure [ <user> public method {closure:%s:%d} ] {
+  @@ %s 25 - 25
+
+  - Parameters [1] {
+    Parameter #0 [ <required> object $o ]
+  }
+  - Return [ self ]
+}
+
+object(C)#%d (0) {
+}
+object(D)#%d (0) {
+}
+C::getSelf(): Return value must be of type C, stdClass returned
+# C::getStatic():
+Closure [ <user> public method {closure:%s:%d} ] {
+  @@ %s 39 - 39
+
+  - Parameters [1] {
+    Parameter #0 [ <required> object $o ]
+  }
+  - Return [ static ]
+}
+
+object(C)#%d (0) {
+}
+object(D)#%d (0) {
+}
+C::getStatic(): Return value must be of type C, stdClass returned
+# D::getSelf():
+Closure [ <user> public method {closure:%s:%d} ] {
+  @@ %s 55 - 55
+
+  - Parameters [1] {
+    Parameter #0 [ <required> object $o ]
+  }
+  - Return [ self ]
+}
+
+object(D)#%d (0) {
+}
+object(C)#%d (0) {
+}
+TypeError: C::getSelf(): Return value must be of type C, stdClass returned
+# D::getStatic():
+Closure [ <user> public method {closure:%s:%d} ] {
+  @@ %s 69 - 69
+
+  - Parameters [1] {
+    Parameter #0 [ <required> object $o ]
+  }
+  - Return [ static ]
+}
+
+object(D)#%d (0) {
+}
+TypeError: C::getStatic(): Return value must be of type D, C returned
+C::getStatic(): Return value must be of type D, stdClass returned
diff --git a/Zend/tests/partial_application/return_type.phpt b/Zend/tests/partial_application/return_type.phpt
new file mode 100644
index 00000000000..ae3738e2c66
--- /dev/null
+++ b/Zend/tests/partial_application/return_type.phpt
@@ -0,0 +1,20 @@
+--TEST--
+PFA inherits return type
+--FILE--
+<?php
+function foo($a) : array {}
+
+echo (string) new ReflectionFunction(foo(new stdClass, ...));
+?>
+--EXPECTF--
+Closure [ <user> static function {closure:%s:%d} ] {
+  @@ %s 4 - 4
+
+  - Bound Variables [1] {
+      Variable #0 [ $a ]
+  }
+
+  - Parameters [0] {
+  }
+  - Return [ array ]
+}
diff --git a/Zend/tests/partial_application/rfc_examples.inc b/Zend/tests/partial_application/rfc_examples.inc
new file mode 100644
index 00000000000..53babc07888
--- /dev/null
+++ b/Zend/tests/partial_application/rfc_examples.inc
@@ -0,0 +1,76 @@
+<?php
+
+function check_equivalence($tests)
+{
+    foreach ($tests as $test => [$pfa, $closure]) {
+        echo "# ", $test, ": ";
+        $pfaReflector = new ReflectionFunction($pfa);
+        $closureReflector = new ReflectionFunction($closure);
+
+        try {
+            if (count($pfaReflector->getParameters()) !== count($closureReflector->getParameters())) {
+                throw new Exception(sprintf(
+                    "Arity does not match: expected %d, got %d",
+                    count($closureReflector->getParameters()),
+                    count($pfaReflector->getParameters()),
+                ));
+            }
+
+            $it = new MultipleIterator();
+            $it->attachIterator(new ArrayIterator($pfaReflector->getParameters()));
+            $it->attachIterator(new ArrayIterator($closureReflector->getParameters()));
+            foreach ($it as $i => [$pfaParam, $closureParam]) {
+                [$i] = $i;
+                if ($pfaParam->getName() !== $closureParam->getName()) {
+                    throw new Exception(sprintf("Name of param %d does not match: %s vs %s",
+                        $i,
+                        $pfaParam->getName(),
+                        $closureParam->getName(),
+                    ));
+                }
+                if ((string)$pfaParam->getType() !== (string)$closureParam->getType()) {
+                    throw new Exception(sprintf("Type of param %d does not match: %s vs %s",
+                        $i,
+                        $pfaParam->getType(),
+                        $closureParam->getType(),
+                    ));
+                }
+                if ($pfaParam->isOptional() !== $closureParam->isOptional()) {
+                    throw new Exception(sprintf("Optionalness of param %d does not match: %d vs %d",
+                        $i,
+                        $pfaParam->isOptional(),
+                        $closureParam->isOptional(),
+                    ));
+                }
+            }
+
+            $args = [];
+            foreach ($pfaReflector->getParameters() as $i => $p) {
+                $args[] = match ((string) $p->getType()) {
+                    'int' => 100 + $i,
+                    'float' => 100.5 + $i,
+                    '?float' => 100.5 + $i,
+                    'string' => (string) (100 + $i),
+                    'Point' => new Point,
+                    'mixed' => "mixed($i)",
+                    '' => "mixed($i)",
+                };
+            }
+
+            if ($pfaReflector->getClosureThis() !== $closureReflector->getClosureThis()) {
+                throw new Exception("\$this differs");
+            }
+
+            if ($pfa(...$args) !== $closure(...$args)) {
+                throw new Exception("PFA is not equivalent to closure");
+            }
+        } catch (Exception $e) {
+            echo $e->getMessage(), "\n";
+            echo $pfaReflector;
+            echo $closureReflector;
+            return;
+        }
+
+        echo "Ok\n";
+    }
+}
diff --git a/Zend/tests/partial_application/rfc_examples_const_expr.phpt b/Zend/tests/partial_application/rfc_examples_const_expr.phpt
new file mode 100644
index 00000000000..e2fc5744025
--- /dev/null
+++ b/Zend/tests/partial_application/rfc_examples_const_expr.phpt
@@ -0,0 +1,25 @@
+--TEST--
+PFA RFC examples: "Constant expressions" section
+--XFAIL--
+PFA in constant expressions not implemented yet
+--FILE--
+<?php
+
+class Foo
+{
+    public function __construct(
+        private \Closure $callback = bar(1, 2, ?),
+    ) {}
+}
+
+class Example
+{
+    public const BASE = 10;
+
+    private Closure $arrayToInt = array_map(intval(?, self::BASE), ?);
+}
+
+?>
+==DONE==
+--EXPECTF--
+==DONE==
diff --git a/Zend/tests/partial_application/rfc_examples_debug.phpt b/Zend/tests/partial_application/rfc_examples_debug.phpt
new file mode 100644
index 00000000000..9c5501090d4
--- /dev/null
+++ b/Zend/tests/partial_application/rfc_examples_debug.phpt
@@ -0,0 +1,25 @@
+--TEST--
+PFA RFC examples: "Debug output" section
+--FILE--
+<?php
+
+function f($a, #[SensitiveParameter] $b, $c) {
+    g();
+}
+
+function g() {
+    throw new Exception();
+}
+
+$f = f(?, ?, 3);
+$f(1, 2);
+
+?>
+--EXPECTF--
+Fatal error: Uncaught Exception in %s:%d
+Stack trace:
+#0 %s(%d): g()
+#1 %s(%d): f(1, Object(SensitiveParameterValue), 3)
+#2 %s(%d): {closure:pfa:%s}(1, Object(SensitiveParameterValue))
+#3 {main}
+  thrown in %s on line %d
diff --git a/Zend/tests/partial_application/rfc_examples_errors.phpt b/Zend/tests/partial_application/rfc_examples_errors.phpt
new file mode 100644
index 00000000000..21818d7446e
--- /dev/null
+++ b/Zend/tests/partial_application/rfc_examples_errors.phpt
@@ -0,0 +1,34 @@
+--TEST--
+PFA RFC examples: "Error examples" section
+--FILE--
+<?php
+
+function stuff(int $i, string $s, float $f, Point $p, int $m = 0) {}
+
+class Point {}
+
+$point = new Point();
+
+try {
+    stuff(?);
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+try {
+    stuff(?, ?, ?, ?, ?, ?);
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+try {
+    stuff(?, ?, 3.5, $point, i: 5);
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+ArgumentCountError: Partial application of stuff() expects at least 4 arguments, 1 given
+ArgumentCountError: Partial application of stuff() expects at most 5 arguments, 6 given
+Error: Named parameter $i overwrites previous placeholder
diff --git a/Zend/tests/partial_application/rfc_examples_eval_order.phpt b/Zend/tests/partial_application/rfc_examples_eval_order.phpt
new file mode 100644
index 00000000000..2fd6c9422e9
--- /dev/null
+++ b/Zend/tests/partial_application/rfc_examples_eval_order.phpt
@@ -0,0 +1,30 @@
+--TEST--
+PFA RFC examples: "Evaluation order" section
+--FILE--
+<?php
+
+function getArg() {
+    print __FUNCTION__ . PHP_EOL;
+    return 'hi';
+}
+
+function speak(string $who, string $msg) {
+    printf("%s: %s\n", $who, $msg);
+}
+
+$arrow = static fn($who) => speak($who, getArg());
+print "Arnaud\n";
+$arrow('Larry');
+
+$partial = speak(?, getArg());
+print "Arnaud\n";
+$partial('Larry');
+
+?>
+--EXPECT--
+Arnaud
+getArg
+Larry: hi
+getArg
+Arnaud
+Larry: hi
diff --git a/Zend/tests/partial_application/rfc_examples_extra_args.phpt b/Zend/tests/partial_application/rfc_examples_extra_args.phpt
new file mode 100644
index 00000000000..f6a24df8cc7
--- /dev/null
+++ b/Zend/tests/partial_application/rfc_examples_extra_args.phpt
@@ -0,0 +1,54 @@
+--TEST--
+PFA RFC examples: "Variadics, func_get_args(), and extraneous arguments" section
+--FILE--
+<?php
+
+require 'rfc_examples.inc';
+
+function foo(int $i, int $j) {
+    return func_num_args();
+}
+
+function foo2(int $i, int $j, ...$extra) {
+    return func_num_args();
+}
+
+$tests = [
+    'If a PFA call has no ... placeholder, then any extraneous arguments to the resulting closure will be ignored. That is consistent with how manually writing the equivalent closure would behave, and is the same regardless of whether the underlying function is variadic' => [
+        foo(1, ?),
+        static function (int $j) { return foo(1, $j); },
+    ],
+    'If a PFA call has a ... placeholder, then any extraneous arguments will be passed through to the underlying function' => [
+        foo(1, ?, ...),
+        static function (int $j) { return foo(1, $j, ...array_slice(func_get_args(), 1)); },
+    ],
+    'If a PFA call has a ... placeholder and the underlying function is variadic, then the trailing arguments will be forwarded directly but will get “collected” by the variadic parameter as normal' => [
+        foo2(1, ?, ...),
+        static function (int $j, ...$extra) { return foo2(1, $j, ...$extra); },
+    ],
+];
+
+check_equivalence($tests);
+
+echo "# The extra parameter here will be passed to the closure object, which will simply ignore it:\n";
+var_dump(foo(1, ?)(4, 'ignore me'));
+
+echo "# The extra parameter here will be passed to the closure object, which will forward it directly to the underlying function.  It will be accessible only via ''func_get_args()'' et al:\n";
+var_dump(foo(1, ?, ...)(4, 'ignore me'));
+
+echo "# The extra parameter here will be passed to the closure object, which will forward it directly to the underlying function.  It will show up as part of the \$extra array:\n";
+var_dump(foo2(1, ?, ...)(4, 'ignore me'));
+
+?>
+==DONE==
+--EXPECT--
+# If a PFA call has no ... placeholder, then any extraneous arguments to the resulting closure will be ignored. That is consistent with how manually writing the equivalent closure would behave, and is the same regardless of whether the underlying function is variadic: Ok
+# If a PFA call has a ... placeholder, then any extraneous arguments will be passed through to the underlying function: Ok
+# If a PFA call has a ... placeholder and the underlying function is variadic, then the trailing arguments will be forwarded directly but will get “collected” by the variadic parameter as normal: Ok
+# The extra parameter here will be passed to the closure object, which will simply ignore it:
+int(2)
+# The extra parameter here will be passed to the closure object, which will forward it directly to the underlying function.  It will be accessible only via ''func_get_args()'' et al:
+int(3)
+# The extra parameter here will be passed to the closure object, which will forward it directly to the underlying function.  It will show up as part of the $extra array:
+int(3)
+==DONE==
diff --git a/Zend/tests/partial_application/rfc_examples_incompatible_functions.phpt b/Zend/tests/partial_application/rfc_examples_incompatible_functions.phpt
new file mode 100644
index 00000000000..dd99cb229ae
--- /dev/null
+++ b/Zend/tests/partial_application/rfc_examples_incompatible_functions.phpt
@@ -0,0 +1,14 @@
+--TEST--
+PFA RFC examples: "Incompatible functions" section
+--FILE--
+<?php
+
+try {
+    (function ($f) { $f(?); })("func_get_arg");
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+Error: Cannot call func_get_arg() dynamically
diff --git a/Zend/tests/partial_application/rfc_examples_magic_methods.phpt b/Zend/tests/partial_application/rfc_examples_magic_methods.phpt
new file mode 100644
index 00000000000..7162a790c57
--- /dev/null
+++ b/Zend/tests/partial_application/rfc_examples_magic_methods.phpt
@@ -0,0 +1,47 @@
+--TEST--
+PFA RFC examples: "Magic methods" section
+--FILE--
+<?php
+
+require 'rfc_examples.inc';
+
+class Foo {
+    public function __call($method, $args) {
+        printf("%s::%s\n", __CLASS__, $method);
+        print_r($args);
+    }
+}
+
+$f = new Foo();
+
+check_equivalence([
+    'Test 1' => [
+        $f->method(?, ?),
+        (function ($f) {
+            return fn(mixed $arguments0, mixed $arguments1) => $f->method($arguments0, $arguments1);
+        })($f)->bindTo($f),
+    ],
+]);
+
+try {
+    $f->method(?, ?)(a: 1, b: 2);
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+# Test 1: Foo::method
+Array
+(
+    [0] => mixed(0)
+    [1] => mixed(1)
+)
+Foo::method
+Array
+(
+    [0] => mixed(0)
+    [1] => mixed(1)
+)
+Ok
+Error: Unknown named parameter $a
diff --git a/Zend/tests/partial_application/rfc_examples_overview.phpt b/Zend/tests/partial_application/rfc_examples_overview.phpt
new file mode 100644
index 00000000000..b6fc8c0586c
--- /dev/null
+++ b/Zend/tests/partial_application/rfc_examples_overview.phpt
@@ -0,0 +1,44 @@
+--TEST--
+PFA RFC examples: "Overview" section
+--FILE--
+<?php
+
+require 'rfc_examples.inc';
+
+function foo(int $a, int $b, int $c, int $d): int
+{
+    return $a + $b + $c + $d;
+}
+
+$tests = [
+    'Test 1' => [
+        foo(1, ?, 3, 4),
+        static fn(int $b): int => foo(1, $b, 3, 4),
+    ],
+    'Test 2' => [
+        foo(1, ?, 3, ?),
+        static fn(int $b, int $d): int => foo(1, $b, 3, $d),
+    ],
+    'Test 3' => [
+        foo(1, ...),
+        static fn(int $b, int $c, int $d): int => foo(1, $b, $c, $d),
+    ],
+    'Test 4' => [
+        foo(1, 2, ...),
+        static fn(int $c, int $d): int => foo(1, 2, $c, $d),
+    ],
+    'Test 5' => [
+        foo(1, ?, 3, ...),
+        static fn(int $b, int $d): int => foo(1, $b, 3, $d),
+    ],
+];
+
+check_equivalence($tests);
+
+?>
+--EXPECT--
+# Test 1: Ok
+# Test 2: Ok
+# Test 3: Ok
+# Test 4: Ok
+# Test 5: Ok
diff --git a/Zend/tests/partial_application/rfc_examples_scoping.phpt b/Zend/tests/partial_application/rfc_examples_scoping.phpt
new file mode 100644
index 00000000000..232d225dca6
--- /dev/null
+++ b/Zend/tests/partial_application/rfc_examples_scoping.phpt
@@ -0,0 +1,113 @@
+--TEST--
+PFA RFC examples: "Scoping" section
+--FILE--
+<?php
+
+require 'rfc_examples.inc';
+
+function foo(int $i, int $j = 0): string {
+    return "$i $j";
+}
+
+class Foo {
+    static function bar(int $i, int $j): string {
+        return "$i $j";
+    }
+}
+
+class C {
+    function f($a) {
+        return self::class;
+    }
+}
+class CSubClass extends C {
+    function g($a) {
+        return self::class;
+    }
+
+    function h($a): \Closure {
+        return parent::f(?);
+    }
+}
+class Unrelated {}
+
+$tests = [
+    'Static closure 1' => [
+        foo(?, ?),
+        static fn(int $i, int $j): string => foo($i, $j),
+    ],
+    'Static closure 2' => [
+        Foo::bar(?, ?),
+        static fn(int $i, int $j): string => Foo::bar($i, $j),
+    ],
+    'Static closure 3' => [
+        foo(?, ?)(1, ?),
+        static fn(int $j): string => foo(1, $j),
+    ],
+    'Static closure 4' => [
+        foo(...)(?),
+        static fn(int $i): string => foo($i, 0),
+    ],
+];
+
+check_equivalence($tests);
+
+$c = new C();
+$f = $c->f(?);
+
+echo "# Cannot unbind \$this:\n";
+var_dump($f->bindTo(null, C::class)); // Warning: Cannot unbind $this of method, this will be an error in PHP 9 (returns null)
+
+echo "# Cannot rebind scope:\n";
+var_dump($f->bindTo($c, CSubClass::class)); // Warning: Cannot rebind scope of closure created from method, this will be an error in PHP 9 (returns null)
+
+echo "# Can rebind \$this with subclass:\n";
+var_dump($f->bindTo(new CSubClass, C::class)); // Allowed
+
+echo "# Cannot rebind \$this with unrelated class:\n";
+$f = $f->bindTo(new Unrelated, C::class); // Warning: Cannot bind method C::{closure:/path/to/test.php:11}() to object of class Unrelated, this will be an error in PHP 9 (returns null)
+
+echo "# self resolution:\n";
+$c = new CSubClass();
+var_dump($c->f(?)(1)); // string(1) "C"
+var_dump($c->g(?)(1)); // string(9) "CSubClass"
+var_dump($c->h(1)(1)); // string(1) "C"
+
+?>
+--EXPECTF--
+# Static closure 1: Ok
+# Static closure 2: Ok
+# Static closure 3: Ok
+# Static closure 4: Ok
+# Cannot unbind $this:
+
+Warning: Cannot unbind $this of method, this will be an error in PHP 9 in %s on line %d
+NULL
+# Cannot rebind scope:
+
+Warning: Cannot rebind scope of closure created from method, this will be an error in PHP 9 in %s on line %d
+NULL
+# Can rebind $this with subclass:
+object(Closure)#%d (5) {
+  ["name"]=>
+  string(%d) "{closure:pfa:%s}"
+  ["file"]=>
+  string(%d) "%s"
+  ["line"]=>
+  int(53)
+  ["this"]=>
+  object(CSubClass)#%d (0) {
+  }
+  ["parameter"]=>
+  array(1) {
+    ["$a"]=>
+    string(10) "<required>"
+  }
+}
+# Cannot rebind $this with unrelated class:
+
+Warning: Cannot bind method C::{closure:pfa:%s}() to object of class Unrelated, this will be an error in PHP 9 in %s on line %d
+# self resolution:
+string(1) "C"
+string(9) "CSubClass"
+string(1) "C"
diff --git a/Zend/tests/partial_application/rfc_examples_semantics.phpt b/Zend/tests/partial_application/rfc_examples_semantics.phpt
new file mode 100644
index 00000000000..a3ea25c30d6
--- /dev/null
+++ b/Zend/tests/partial_application/rfc_examples_semantics.phpt
@@ -0,0 +1,30 @@
+--TEST--
+PFA RFC examples: "Placeholder Semantics" section
+--FILE--
+<?php
+
+require 'rfc_examples.inc';
+
+class Point {}
+
+function foo(int $a = 5, int $b = 1, string ...$c) { }
+
+function stuff(int $i, string $s, float $f, Point $p, int $m = 0) {}
+
+$tests = [
+    'Test 1' => [
+        foo(?, ?, ?, ?),
+        static fn(int $a, int $b, string $c0, string $c1) => foo($a, $b, $c0, $c1),
+    ],
+    'Test 2' => [
+        stuff(1, ?, p: ?, f: 3.14, ...),
+        static fn(string $s, Point $p, int $m = 0) => stuff(1, $s, 3.14, $p, $m),
+    ],
+];
+
+check_equivalence($tests);
+
+?>
+--EXPECT--
+# Test 1: Ok
+# Test 2: Ok
diff --git a/Zend/tests/partial_application/rfc_examples_semantics_examples.phpt b/Zend/tests/partial_application/rfc_examples_semantics_examples.phpt
new file mode 100644
index 00000000000..d8c4a83ca07
--- /dev/null
+++ b/Zend/tests/partial_application/rfc_examples_semantics_examples.phpt
@@ -0,0 +1,217 @@
+--TEST--
+PFA RFC examples: "Placeholder Semantics: Examples" section
+--FILE--
+<?php
+
+require 'rfc_examples.inc';
+
+class Point {}
+
+function stuff(int $i1, string $s2, float $f3, Point $p4, int $m5 = 0): array {
+    return [$i1, $s2, $f3, $p4, $m5];
+}
+
+function things(int $i1, ?float $f3 = null, Point ...$points): array {
+    return [$i1, $f3, $points];
+}
+
+function four(int $a, int $b, int $c, int $d): string {
+    return "$a, $b, $c, $d\n";
+}
+
+class E {
+    private static $cache = [];
+
+    public function __construct(private int $x, private int $y) {}
+
+    public static function make(int $x, int $y): self {
+        return self::$cache["$x-$y"] ??= new self($x, $y);
+    }
+
+    public function foo(int $a, int $b, int $c): array {
+        return [$a, $b, $c];
+    }
+}
+
+class RunMe {
+    public function __invoke(int $a, int $b): string {
+        return "$a $b";
+    }
+}
+
+$point = new Point();
+
+$tests = [
+    'Manually specify the first two values, and pull the rest "as is"' => [
+        stuff(?, ?, ...),
+        static fn(int $i1, string $s2, float $f3, Point $p4, int $m5 = 0): array
+          => stuff($i1, $s2, $f3, $p4, $m5),
+    ],
+    'The degenerate "first class callables" case. (Supported since 8.1)' => [
+        stuff(...),
+        static fn(int $i1, string $s2, float $f3, Point $p4, int $m5 = 0): array
+          => stuff($i1, $s2, $f3, $p4, $m5),
+    ],
+    'Provide some values, require the rest to be provided later (1)' => [
+        stuff(1, 'hi', ?, ?, ?),
+        static fn(float $f3, Point $p4, int $m5): array => stuff(1, 'hi', $f3, $p4, $m5),
+    ],
+    'Provide some values, require the rest to be provided later (2)' => [
+        stuff(1, 'hi', ...),
+        static fn(float $f3, Point $p4, int $m5 = 0): array => stuff(1, 'hi', $f3, $p4, $m5),
+    ],
+    'Provide some values, but not just from the left (1)' => [
+        stuff(1, ?, 3.5, ?, ?),
+        static fn(string $s2, Point $p4, int $m5): array => stuff(1, $s2, 3.5, $p4, $m5),
+    ],
+    'Provide some values, but not just from the left (2)' => [
+        stuff(1, ?, 3.5, ...),
+        static fn(string $s2, Point $p4, int $m5 = 0): array => stuff(1, $s2, 3.5, $p4, $m5),
+    ],
+    'Provide just the last value' => [
+        stuff(?, ?, ?, ?, 5),
+        static fn(int $i1, string $s2, float $f3, Point $p4): array
+          => stuff($i1, $s2, $f3, $p4, 5),
+    ],
+    'Not accounting for an optional argument means it will always get its default value' => [
+        stuff(?, ?, ?, ?),
+        static fn(int $i1, string $s2, float $f3, Point $p4): array
+          => stuff($i1, $s2, $f3, $p4),
+    ],
+    'Named arguments can be pulled "out of order", and still work (1)' => [
+        stuff(?, ?, f3: 3.5, p4: $point),
+        static fn(int $i1, string $s2): array => stuff($i1, $s2, 3.5, $point),
+    ],
+    'Named arguments can be pulled "out of order", and still work (2)' => [
+        stuff(?, ?, p4: $point, f3: 3.5),
+        static fn(int $i1, string $s2): array => stuff($i1, $s2, 3.5, $point),
+    ],
+
+    'But named placeholders adopt the order listed' => [
+        stuff(s2: ?, i1: ?, p4: ?, f3: 3.5),
+        static fn(string $s2, int $i1, Point $p4): array => stuff($i1, $s2, 3.5, $p4),
+    ],
+    'The ... "everything else" placeholder respects named arguments' => [
+        stuff(?, ?, f3: 3.5, p4: $point, ...),
+        static fn(int $i1, string $s2, int $m5 = 0): array => stuff($i1, $s2, 3.5, $point, $m5),
+    ],
+    'Prefill all parameters, making a "delayed call" or "thunk"' => [
+        stuff(1, 'hi', 3.4, $point, 5, ...),
+        static fn(): array => stuff(1, 'hi', 3.4, $point, 5),
+    ],
+
+    // Variadics
+
+    'FCC equivalent.  The signature is unchanged' => [
+        things(...),
+        static fn(int $i1, ?float $f3 = null, Point ...$points): array => things(...[$i1, $f3, ...$points]),
+    ],
+    'Provide some values, but allow the variadic to remain variadic' => [
+        things(1, 3.14, ...),
+        static fn(Point ...$points): array => things(1, 3.14, ...$points),
+    ],
+    'In this version, the partial requires precisely four arguments, the last two of which will get received by things() in the variadic parameter. Note too that $f becomes required in this case' => [
+        things(?, ?, ?, ?),
+        static fn(int $i1, ?float $f3, Point $points0, Point $points1): array => things($i1, $f3, $points0, $points1),
+    ],
+
+    // Esoteric examples
+
+    'Esoteric 1' => [
+        four(...),
+        static fn(int $a, int $b, int $c, int $d): string => four($a, $b, $c, $d),
+    ],
+    'Esoteric 2' => [
+        four(1, 2, ...),
+        static fn(int $c, int $d): string => four(1, 2, $c, $d),
+    ],
+    'Esoteric 3' => [
+        four(1, 2, 3, ?),
+        static fn(int $d): string => four(1, 2, 3, $d),
+    ],
+    'Esoteric 4' => [
+        four(1, ?, ?, 4),
+        static fn(int $b, int $c): string => four(1, $b, $c, 4),
+    ],
+    'Esoteric 5' => [
+        four(1, 2, 3, 4, ...),
+        static fn(): string => four(1, 2, 3, 4, ...array_slice(func_get_args(), 4)),
+    ],
+    'Esoteric 6' => [
+        four(d: 4, a: 1, ...),
+        static fn(int $b, int $c): string => four(1, $b, $c, 4, ...array_slice(func_get_args(), 4)),
+    ],
+    'Esoteric 7' => [
+        four(c: ?, d: 4, b: ?, a: 1),
+        static fn(int $c, int $b): string => four(1, $b, $c, 4, ...array_slice(func_get_args(), 4)),
+    ],
+
+    // Other callable styles
+
+    'This is allowed. Note the method is static, thus the partial closure is static' => [
+        E::make(1, ?),
+        static fn(int $y): E => E::make(1, $y),
+    ],
+    'Note the method is non-static, so the partial closure is non-static' => (function () {
+        $eMaker = E::make(1, ?);
+        $e = $eMaker(2);
+        return [
+            $e->foo(?, ?, 3),
+            (function ($e) {
+                return fn(int $a, int $b): array => $e->foo($a, $b, 3);
+            })($e)->bindTo($e),
+        ];
+    })(),
+    '$c can then be further refined' => (function () {
+        $eMaker = E::make(1, ?);
+        $e = $eMaker(2);
+        $c = $e->foo(?, ?, 3);
+        return [
+            $c(1, ?),
+            (function ($e) {
+                return fn(int $b): array => $e->foo(1, $b, 3);
+            })($e)->bindTo($e),
+        ];
+    })(),
+    'RunMe' => (function () {
+        $r = new RunMe();
+        return [
+            $r(?, 3),
+            (function ($r) {
+                return fn(int $a): string => $r($a, 3);
+            })($r)->bindTo($r),
+        ];
+    })(),
+];
+
+check_equivalence($tests);
+
+?>
+--EXPECT--
+# Manually specify the first two values, and pull the rest "as is": Ok
+# The degenerate "first class callables" case. (Supported since 8.1): Ok
+# Provide some values, require the rest to be provided later (1): Ok
+# Provide some values, require the rest to be provided later (2): Ok
+# Provide some values, but not just from the left (1): Ok
+# Provide some values, but not just from the left (2): Ok
+# Provide just the last value: Ok
+# Not accounting for an optional argument means it will always get its default value: Ok
+# Named arguments can be pulled "out of order", and still work (1): Ok
+# Named arguments can be pulled "out of order", and still work (2): Ok
+# But named placeholders adopt the order listed: Ok
+# The ... "everything else" placeholder respects named arguments: Ok
+# Prefill all parameters, making a "delayed call" or "thunk": Ok
+# FCC equivalent.  The signature is unchanged: Ok
+# Provide some values, but allow the variadic to remain variadic: Ok
+# In this version, the partial requires precisely four arguments, the last two of which will get received by things() in the variadic parameter. Note too that $f becomes required in this case: Ok
+# Esoteric 1: Ok
+# Esoteric 2: Ok
+# Esoteric 3: Ok
+# Esoteric 4: Ok
+# Esoteric 5: Ok
+# Esoteric 6: Ok
+# Esoteric 7: Ok
+# This is allowed. Note the method is static, thus the partial closure is static: Ok
+# Note the method is non-static, so the partial closure is non-static: Ok
+# $c can then be further refined: Ok
+# RunMe: Ok
diff --git a/Zend/tests/partial_application/static_method_001.phpt b/Zend/tests/partial_application/static_method_001.phpt
new file mode 100644
index 00000000000..ce4151441c3
--- /dev/null
+++ b/Zend/tests/partial_application/static_method_001.phpt
@@ -0,0 +1,18 @@
+--TEST--
+PFA supports static methods
+--FILE--
+<?php
+class Foo {
+    public static function method($a, $b) {
+        printf("%s\n", __METHOD__);
+    }
+}
+
+$foo = Foo::method(new stdClass, ...);
+
+$bar = $foo(new stdClass, ...);
+
+$bar();
+?>
+--EXPECTF--
+Foo::method
diff --git a/Zend/tests/partial_application/static_pfa_001.phpt b/Zend/tests/partial_application/static_pfa_001.phpt
new file mode 100644
index 00000000000..bc0f65b1ffd
--- /dev/null
+++ b/Zend/tests/partial_application/static_pfa_001.phpt
@@ -0,0 +1,45 @@
+--TEST--
+PFA of static method is static
+--FILE--
+<?php
+
+class C {
+    public static function f($a) {
+        var_dump($a);
+    }
+}
+
+$f = C::f(?);
+echo new ReflectionFunction($f), "\n";
+$f(1);
+
+$f = (new C)->f(?);
+echo new ReflectionFunction($f), "\n";
+$f(1);
+
+// Warns
+var_dump($f->bindTo(new C));
+
+?>
+--EXPECTF--
+Closure [ <user> static public method {closure:pfa:%s:9} ] {
+  @@ %s 9 - 9
+
+  - Parameters [1] {
+    Parameter #0 [ <required> $a ]
+  }
+}
+
+int(1)
+Closure [ <user> static public method {closure:pfa:%s:13} ] {
+  @@ %s 13 - 13
+
+  - Parameters [1] {
+    Parameter #0 [ <required> $a ]
+  }
+}
+
+int(1)
+
+Warning: Cannot bind an instance to a static closure, this will be an error in PHP 9 in %s on line %d
+NULL
diff --git a/Zend/tests/partial_application/static_pfa_002.phpt b/Zend/tests/partial_application/static_pfa_002.phpt
new file mode 100644
index 00000000000..5222109958b
--- /dev/null
+++ b/Zend/tests/partial_application/static_pfa_002.phpt
@@ -0,0 +1,35 @@
+--TEST--
+PFA of instance method is not static
+--FILE--
+<?php
+
+class C {
+    public function f($a) {
+        var_dump($this, $this === $a);
+    }
+}
+
+$c = new C();
+$f = $c->f(?);
+echo new ReflectionFunction($f), "\n";
+$f($c);
+
+$c2 = new C();
+$f->bindTo($c2)($c2);
+
+?>
+--EXPECTF--
+Closure [ <user> public method {closure:pfa:%s:10} ] {
+  @@ %s 10 - 10
+
+  - Parameters [1] {
+    Parameter #0 [ <required> $a ]
+  }
+}
+
+object(C)#%d (0) {
+}
+bool(true)
+object(C)#%d (0) {
+}
+bool(true)
diff --git a/Zend/tests/partial_application/static_pfa_003.phpt b/Zend/tests/partial_application/static_pfa_003.phpt
new file mode 100644
index 00000000000..e7b3c187e18
--- /dev/null
+++ b/Zend/tests/partial_application/static_pfa_003.phpt
@@ -0,0 +1,30 @@
+--TEST--
+PFA of standalone function is static
+--FILE--
+<?php
+
+function f($a) {
+    var_dump($a);
+}
+
+$f = f(?);
+echo new ReflectionFunction($f), "\n";
+$f(1);
+
+// Warns
+var_dump($f->bindTo(new class {}));
+
+?>
+--EXPECTF--
+Closure [ <user> static function {closure:pfa:%s:7} ] {
+  @@ %s 7 - 7
+
+  - Parameters [1] {
+    Parameter #0 [ <required> $a ]
+  }
+}
+
+int(1)
+
+Warning: Cannot bind an instance to a static closure, this will be an error in PHP 9 in %s on line %d
+NULL
diff --git a/Zend/tests/partial_application/static_pfa_004.phpt b/Zend/tests/partial_application/static_pfa_004.phpt
new file mode 100644
index 00000000000..093430232d4
--- /dev/null
+++ b/Zend/tests/partial_application/static_pfa_004.phpt
@@ -0,0 +1,44 @@
+--TEST--
+PFA of non-static closure is not static
+--FILE--
+<?php
+
+$c = function ($a) {
+    try {
+        var_dump($this, $this === $a);
+    } catch (Error $e) {
+        echo $e::class, ": ", $e->getMessage(), "\n";
+    }
+};
+
+echo "# Original PFA\n";
+
+$f = $c(?);
+echo new ReflectionFunction($f), "\n";
+$f($c);
+
+echo "# Re-bound PFA\n";
+
+$c2 = new class {};
+$f->bindTo($c2)($c2);
+
+?>
+--EXPECTF--
+# Original PFA
+Closure [ <user> function {closure:pfa:%s:13} ] {
+  @@ %s 13 - 13
+
+  - Bound Variables [1] {
+      Variable #0 [ $fn ]
+  }
+
+  - Parameters [1] {
+    Parameter #0 [ <required> $a ]
+  }
+}
+
+Error: Using $this when not in object context
+# Re-bound PFA
+object(class@anonymous)#3 (0) {
+}
+bool(true)
diff --git a/Zend/tests/partial_application/static_pfa_005.phpt b/Zend/tests/partial_application/static_pfa_005.phpt
new file mode 100644
index 00000000000..2783485b2f4
--- /dev/null
+++ b/Zend/tests/partial_application/static_pfa_005.phpt
@@ -0,0 +1,34 @@
+--TEST--
+PFA of static closure is static
+--FILE--
+<?php
+
+$c = static function ($a) {
+    var_dump($a);
+};
+
+$f = $c(?);
+echo new ReflectionFunction($f), "\n";
+$f(1);
+
+// Warns
+var_dump($f->bindTo(new class {}));
+
+?>
+--EXPECTF--
+Closure [ <user> static function {closure:pfa:%s:7} ] {
+  @@ %s 7 - 7
+
+  - Bound Variables [1] {
+      Variable #0 [ $fn ]
+  }
+
+  - Parameters [1] {
+    Parameter #0 [ <required> $a ]
+  }
+}
+
+int(1)
+
+Warning: Cannot bind an instance to a static closure, this will be an error in PHP 9 in %s on line %d
+NULL
diff --git a/Zend/tests/partial_application/static_polymorphism.phpt b/Zend/tests/partial_application/static_polymorphism.phpt
new file mode 100644
index 00000000000..5f121c65c0e
--- /dev/null
+++ b/Zend/tests/partial_application/static_polymorphism.phpt
@@ -0,0 +1,43 @@
+--TEST--
+PFA: static polymorphism
+--FILE--
+<?php
+
+class P {
+    public static function m(string $a): void {
+        echo __METHOD__, PHP_EOL;
+        var_dump($a);
+    }
+
+
+    public static function get() {
+        /* The method is resolved before creating the PFA, and captured by the
+         * PFA. We use the signature of the resolved method. */
+        return static::m(?);
+    }
+}
+
+class C extends P {
+    public static function m(string|array $b): void {
+        echo __METHOD__, PHP_EOL;
+        var_dump($b);
+    }
+}
+
+for ($i = 0; $i < 2; $i++) {
+    P::get()(a: 'a');
+    C::get()(b: []);
+}
+
+?>
+--EXPECT--
+P::m
+string(1) "a"
+C::m
+array(0) {
+}
+P::m
+string(1) "a"
+C::m
+array(0) {
+}
diff --git a/Zend/tests/partial_application/static_vars_001.phpt b/Zend/tests/partial_application/static_vars_001.phpt
new file mode 100644
index 00000000000..610cddf37b5
--- /dev/null
+++ b/Zend/tests/partial_application/static_vars_001.phpt
@@ -0,0 +1,21 @@
+--TEST--
+PFA static variables are shared (001)
+--FILE--
+<?php
+function foo($a) {
+    static $var = 0;
+
+    ++$var;
+
+    return $var;
+}
+
+var_dump(foo(new stdClass));
+
+$foo = foo(new stdClass, ...);
+
+var_dump($foo());
+?>
+--EXPECT--
+int(1)
+int(2)
diff --git a/Zend/tests/partial_application/static_vars_002.phpt b/Zend/tests/partial_application/static_vars_002.phpt
new file mode 100644
index 00000000000..4e6cc82b720
--- /dev/null
+++ b/Zend/tests/partial_application/static_vars_002.phpt
@@ -0,0 +1,22 @@
+--TEST--
+PFA static variables are shared (002)
+--FILE--
+<?php
+$closure = function ($a) {
+    static $var = 0;
+
+    ++$var;
+
+    return $var;
+};
+
+var_dump($closure(new stdClass));
+
+$foo = $closure(new stdClass, ...);
+$closure = null;
+
+var_dump($foo());
+?>
+--EXPECT--
+int(1)
+int(2)
diff --git a/Zend/tests/partial_application/static_vars_003.phpt b/Zend/tests/partial_application/static_vars_003.phpt
new file mode 100644
index 00000000000..04328bf717b
--- /dev/null
+++ b/Zend/tests/partial_application/static_vars_003.phpt
@@ -0,0 +1,25 @@
+--TEST--
+PFA static variables are shared (003)
+--FILE--
+<?php
+$closure = function ($a) {
+    static $var = 0;
+
+    ++$var;
+
+    return $var;
+};
+
+var_dump($closure(new stdClass));
+
+$foo = $closure(?);
+$closure = null;
+
+$bar = $foo(?);
+$foo = null;
+
+var_dump($bar(new stdClass));
+?>
+--EXPECT--
+int(1)
+int(2)
diff --git a/Zend/tests/partial_application/superfluous_args_are_forwarded.phpt b/Zend/tests/partial_application/superfluous_args_are_forwarded.phpt
new file mode 100644
index 00000000000..50609265521
--- /dev/null
+++ b/Zend/tests/partial_application/superfluous_args_are_forwarded.phpt
@@ -0,0 +1,44 @@
+--TEST--
+PFAs forwards superfluous args iff a variadic placeholder is specified
+--FILE--
+<?php
+
+function f($a) {
+    var_dump(func_get_args());
+}
+
+$f = f(?, ...);
+$f(1, 2, 3);
+
+$h = f(?);
+$h(1, 2, 3);
+
+function g($a,  ...$args) {
+    var_dump(func_get_args());
+}
+
+$g = g(?, ...);
+$g(1, 2, 3);
+
+?>
+--EXPECT--
+array(3) {
+  [0]=>
+  int(1)
+  [1]=>
+  int(2)
+  [2]=>
+  int(3)
+}
+array(1) {
+  [0]=>
+  int(1)
+}
+array(3) {
+  [0]=>
+  int(1)
+  [1]=>
+  int(2)
+  [2]=>
+  int(3)
+}
diff --git a/Zend/tests/partial_application/this.phpt b/Zend/tests/partial_application/this.phpt
new file mode 100644
index 00000000000..bda6900bae3
--- /dev/null
+++ b/Zend/tests/partial_application/this.phpt
@@ -0,0 +1,20 @@
+--TEST--
+PFA $this
+--FILE--
+<?php
+class Foo {
+    public function method($a, $b) {
+        return $this;
+    }
+}
+
+$foo = new Foo;
+
+$bar = $foo->method(new stdClass, ...);
+
+$baz = $bar(new stdClass, ...);
+
+var_dump($foo === $baz());
+?>
+--EXPECT--
+bool(true)
diff --git a/Zend/tests/partial_application/variation_call_001.phpt b/Zend/tests/partial_application/variation_call_001.phpt
new file mode 100644
index 00000000000..b9c1c19ec9c
--- /dev/null
+++ b/Zend/tests/partial_application/variation_call_001.phpt
@@ -0,0 +1,31 @@
+--TEST--
+PFA variation: call
+--FILE--
+<?php
+class Param {
+
+    public function __toString() {
+        return __CLASS__;
+    }
+}
+
+class Foo {
+    public function method($a, $b) {
+        printf("%s: %s, %s\n", get_called_class(), $a, $b);
+    }
+}
+
+class Bar extends Foo {
+
+}
+
+$bar = new Bar;
+$closure = $bar->method(?, new Param);
+
+$closure(1);
+
+$closure->call(/* newThis: */ new Foo(), 10);
+?>
+--EXPECT--
+Bar: 1, Param
+Foo: 10, Param
diff --git a/Zend/tests/partial_application/variation_closure_001.phpt b/Zend/tests/partial_application/variation_closure_001.phpt
new file mode 100644
index 00000000000..1d31f22c7f8
--- /dev/null
+++ b/Zend/tests/partial_application/variation_closure_001.phpt
@@ -0,0 +1,40 @@
+--TEST--
+PFA variation: Closure
+--FILE--
+<?php
+$closure = function($a, $b) {
+};
+
+echo (string) new ReflectionFunction($closure(1, ?));
+
+$closure2 = function($a, $fn) {
+};
+
+echo (string) new ReflectionFunction($closure2(1, ?));
+
+?>
+--EXPECTF--
+Closure [ <user> function {closure:%s:%d} ] {
+  @@ %s 5 - 5
+
+  - Bound Variables [2] {
+      Variable #0 [ $fn ]
+      Variable #1 [ $a ]
+  }
+
+  - Parameters [1] {
+    Parameter #0 [ <required> $b ]
+  }
+}
+Closure [ <user> function {closure:pfa:%s:%d} ] {
+  @@ %s 10 - 10
+
+  - Bound Variables [2] {
+      Variable #0 [ $fn2 ]
+      Variable #1 [ $a ]
+  }
+
+  - Parameters [1] {
+    Parameter #0 [ <required> $fn ]
+  }
+}
diff --git a/Zend/tests/partial_application/variation_closure_002.phpt b/Zend/tests/partial_application/variation_closure_002.phpt
new file mode 100644
index 00000000000..41abe0aaab0
--- /dev/null
+++ b/Zend/tests/partial_application/variation_closure_002.phpt
@@ -0,0 +1,28 @@
+--TEST--
+PFA variation: Closure::__invoke()
+--FILE--
+<?php
+$closure = function($a, $b) {
+    var_dump($a, $b);
+};
+
+$function = $closure->__invoke(1, ?);
+
+echo (string) new ReflectionFunction($function);
+
+$function(10);
+?>
+--EXPECTF--
+Closure [ <user> public method {closure:%s:%d} ] {
+  @@ %svariation_closure_002.php 6 - 6
+
+  - Bound Variables [1] {
+      Variable #0 [ $a ]
+  }
+
+  - Parameters [1] {
+    Parameter #0 [ <required> $b ]
+  }
+}
+int(1)
+int(10)
diff --git a/Zend/tests/partial_application/variation_closure_003.phpt b/Zend/tests/partial_application/variation_closure_003.phpt
new file mode 100644
index 00000000000..da567e17985
--- /dev/null
+++ b/Zend/tests/partial_application/variation_closure_003.phpt
@@ -0,0 +1,46 @@
+--TEST--
+PFA variation: Closure::__invoke() with $this
+--FILE--
+<?php
+class Foo {
+    public function bar() {
+        return function($a, $b) {
+            return [$this, func_get_args()];
+        };
+    }
+}
+
+$foo = new Foo;
+
+$closure = $foo->bar();
+
+$function = $closure->__invoke(1, ?);
+
+echo (string) new ReflectionFunction($function);
+
+var_dump($function(10));
+?>
+--EXPECTF--
+Closure [ <user> public method {closure:%s:%d} ] {
+  @@ %svariation_closure_003.php 14 - 14
+
+  - Bound Variables [1] {
+      Variable #0 [ $a ]
+  }
+
+  - Parameters [1] {
+    Parameter #0 [ <required> $b ]
+  }
+}
+array(2) {
+  [0]=>
+  object(Foo)#1 (0) {
+  }
+  [1]=>
+  array(2) {
+    [0]=>
+    int(1)
+    [1]=>
+    int(10)
+  }
+}
diff --git a/Zend/tests/partial_application/variation_debug_001.phpt b/Zend/tests/partial_application/variation_debug_001.phpt
new file mode 100644
index 00000000000..04b63f3c401
--- /dev/null
+++ b/Zend/tests/partial_application/variation_debug_001.phpt
@@ -0,0 +1,40 @@
+--TEST--
+PFA variation: var_dump(), user function
+--FILE--
+<?php
+function bar($a = 1, $b = 2, ...$c) {
+
+}
+
+var_dump(bar(?, new stdClass, 20, new stdClass, four: 4));
+?>
+--EXPECTF--
+object(Closure)#%d (5) {
+  ["name"]=>
+  string(%d) "{closure:%s}"
+  ["file"]=>
+  string(%d) "%svariation_debug_001.php"
+  ["line"]=>
+  int(6)
+  ["static"]=>
+  array(4) {
+    ["b"]=>
+    object(stdClass)#%d (0) {
+    }
+    ["c"]=>
+    int(20)
+    ["c2"]=>
+    object(stdClass)#%d (0) {
+    }
+    ["extra_named_params"]=>
+    array(1) {
+      ["four"]=>
+      int(4)
+    }
+  }
+  ["parameter"]=>
+  array(1) {
+    ["$a"]=>
+    string(10) "<required>"
+  }
+}
diff --git a/Zend/tests/partial_application/variation_debug_002.phpt b/Zend/tests/partial_application/variation_debug_002.phpt
new file mode 100644
index 00000000000..a7c4c2d76e4
--- /dev/null
+++ b/Zend/tests/partial_application/variation_debug_002.phpt
@@ -0,0 +1,49 @@
+--TEST--
+PFA variation: var_dump(), internal function
+--FILE--
+<?php
+var_dump(array_map(?, [1, 2, 3], [4, 5, 6], four: new stdClass, ...));
+?>
+--EXPECTF--
+object(Closure)#%d (5) {
+  ["name"]=>
+  string(%d) "{closure:%s}"
+  ["file"]=>
+  string(%d) "%svariation_debug_002.php"
+  ["line"]=>
+  int(2)
+  ["static"]=>
+  array(3) {
+    ["array"]=>
+    array(3) {
+      [0]=>
+      int(1)
+      [1]=>
+      int(2)
+      [2]=>
+      int(3)
+    }
+    ["arrays2"]=>
+    array(3) {
+      [0]=>
+      int(4)
+      [1]=>
+      int(5)
+      [2]=>
+      int(6)
+    }
+    ["extra_named_params"]=>
+    array(1) {
+      ["four"]=>
+      object(stdClass)#%d (0) {
+      }
+    }
+  }
+  ["parameter"]=>
+  array(2) {
+    ["$callback"]=>
+    string(10) "<required>"
+    ["$arrays"]=>
+    string(10) "<optional>"
+  }
+}
diff --git a/Zend/tests/partial_application/variation_ex_001.phpt b/Zend/tests/partial_application/variation_ex_001.phpt
new file mode 100644
index 00000000000..0f36e90f616
--- /dev/null
+++ b/Zend/tests/partial_application/variation_ex_001.phpt
@@ -0,0 +1,14 @@
+--TEST--
+PFA variation: UAF in cleanup unfinished calls
+--FILE--
+<?php
+function test($a){}
+
+try {
+    test(1, ...)(?);
+} catch (Error $ex) {
+    echo $ex::class, ": ", $ex->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+ArgumentCountError: Partial application of {closure:%s:%d}() expects at most 0 arguments, 1 given
diff --git a/Zend/tests/partial_application/variation_gc_001.phpt b/Zend/tests/partial_application/variation_gc_001.phpt
new file mode 100644
index 00000000000..2db49cc7436
--- /dev/null
+++ b/Zend/tests/partial_application/variation_gc_001.phpt
@@ -0,0 +1,19 @@
+--TEST--
+PFA variation: GC (001)
+--FILE--
+<?php
+#[AllowDynamicProperties]
+class Foo {
+
+    public function __construct($a) {
+        $this->method = self::__construct(new stdClass, ...);
+    }
+}
+
+$foo = new Foo(new stdClass);
+$foo->bar = $foo;
+
+echo "OK";
+?>
+--EXPECT--
+OK
diff --git a/Zend/tests/partial_application/variation_gc_002.phpt b/Zend/tests/partial_application/variation_gc_002.phpt
new file mode 100644
index 00000000000..31c721e8e68
--- /dev/null
+++ b/Zend/tests/partial_application/variation_gc_002.phpt
@@ -0,0 +1,11 @@
+--TEST--
+PFA variation: GC (002)
+--FILE--
+<?php
+$obj = new stdClass;
+$obj->prop = var_dump($obj, ?);
+
+echo "OK";
+?>
+--EXPECT--
+OK
diff --git a/Zend/tests/partial_application/variation_gc_003.phpt b/Zend/tests/partial_application/variation_gc_003.phpt
new file mode 100644
index 00000000000..23cdaa1c666
--- /dev/null
+++ b/Zend/tests/partial_application/variation_gc_003.phpt
@@ -0,0 +1,15 @@
+--TEST--
+PFA variation: GC (003)
+--FILE--
+<?php
+
+function test(...$args) {
+}
+
+$obj = new stdClass;
+$obj->prop = test(?, x: $obj);
+
+echo "OK";
+?>
+--EXPECT--
+OK
diff --git a/Zend/tests/partial_application/variation_invoke_001.phpt b/Zend/tests/partial_application/variation_invoke_001.phpt
new file mode 100644
index 00000000000..9285c08da19
--- /dev/null
+++ b/Zend/tests/partial_application/variation_invoke_001.phpt
@@ -0,0 +1,21 @@
+--TEST--
+PFA variation: __invoke()
+--FILE--
+<?php
+function foo($a, $b) {
+    return $a + $b;
+}
+
+$foo = foo(b: 10, ...);
+
+var_dump($foo->__invoke(32));
+
+try {
+    $foo->nothing();
+} catch (Error $ex) {
+    echo $ex::class, ": ", $ex->getMessage(), "\n";
+}
+?>
+--EXPECT--
+int(42)
+Error: Call to undefined method Closure::nothing()
diff --git a/Zend/tests/partial_application/variation_nocall_001.phpt b/Zend/tests/partial_application/variation_nocall_001.phpt
new file mode 100644
index 00000000000..3fbb3ec8d8c
--- /dev/null
+++ b/Zend/tests/partial_application/variation_nocall_001.phpt
@@ -0,0 +1,12 @@
+--TEST--
+PFA variation: no call args leak
+--FILE--
+<?php
+function test($a, $b) {}
+
+test(?, new stdClass);
+
+echo "OK";
+?>
+--EXPECT--
+OK
diff --git a/Zend/tests/partial_application/variation_nocall_002.phpt b/Zend/tests/partial_application/variation_nocall_002.phpt
new file mode 100644
index 00000000000..cd4823f1bd0
--- /dev/null
+++ b/Zend/tests/partial_application/variation_nocall_002.phpt
@@ -0,0 +1,30 @@
+--TEST--
+PFA variation: no call, order of destruction
+--FILE--
+<?php
+class Foo {
+    function method($a) {}
+}
+class Dtor {
+    public function __construct(public int $id) {}
+    public function __destruct() {
+        echo __METHOD__, " ", $this->id, "\n";
+    }
+}
+$foo = new Foo;
+$f = $foo->method(?);
+$g = $f(?);
+
+$map = new WeakMap();
+$map[$f] = new Dtor(1);
+$map[$g] = new Dtor(2);
+
+unset($f);
+unset($g);
+
+echo "OK";
+?>
+--EXPECT--
+Dtor::__destruct 2
+Dtor::__destruct 1
+OK
diff --git a/Zend/tests/partial_application/variation_parent_001.phpt b/Zend/tests/partial_application/variation_parent_001.phpt
new file mode 100644
index 00000000000..98095f45009
--- /dev/null
+++ b/Zend/tests/partial_application/variation_parent_001.phpt
@@ -0,0 +1,76 @@
+--TEST--
+PFA variation: parent
+--FILE--
+<?php
+class Foo {
+    public function method($a, $b, ...$c) {
+        return function() {
+            return $this;
+        };
+    }
+}
+
+$foo = new Foo();
+$bar = $foo->method(10, ...);
+$baz = $bar(20, ...);
+
+var_dump($baz, $c = $baz(), $c() === $foo);
+?>
+--EXPECTF--
+object(Closure)#%d (6) {
+  ["name"]=>
+  string(%d) "{closure:%s}"
+  ["file"]=>
+  string(%d) "%svariation_parent_001.php"
+  ["line"]=>
+  int(12)
+  ["static"]=>
+  array(2) {
+    ["fn"]=>
+    object(Closure)#%d (6) {
+      ["name"]=>
+      string(%d) "{closure:%s:%d}"
+      ["file"]=>
+      string(%d) "%s"
+      ["line"]=>
+      int(11)
+      ["static"]=>
+      array(1) {
+        ["a"]=>
+        int(10)
+      }
+      ["this"]=>
+      object(Foo)#%d (0) {
+      }
+      ["parameter"]=>
+      array(2) {
+        ["$b"]=>
+        string(10) "<required>"
+        ["$c"]=>
+        string(10) "<optional>"
+      }
+    }
+    ["b"]=>
+    int(20)
+  }
+  ["this"]=>
+  object(Foo)#%d (0) {
+  }
+  ["parameter"]=>
+  array(1) {
+    ["$c"]=>
+    string(10) "<optional>"
+  }
+}
+object(Closure)#%d (4) {
+  ["name"]=>
+  string(25) "{closure:Foo::method():4}"
+  ["file"]=>
+  string(%d) "%s"
+  ["line"]=>
+  int(4)
+  ["this"]=>
+  object(Foo)#%d (0) {
+  }
+}
+bool(true)
diff --git a/Zend/tests/partial_application/variation_scope_001.phpt b/Zend/tests/partial_application/variation_scope_001.phpt
new file mode 100644
index 00000000000..0dcea0921c4
--- /dev/null
+++ b/Zend/tests/partial_application/variation_scope_001.phpt
@@ -0,0 +1,18 @@
+--TEST--
+PFA variation: called scope
+--FILE--
+<?php
+class Foo {
+    public function method($a) {
+        printf("%s::%s\n", get_called_class(), __FUNCTION__);
+    }
+}
+
+$foo = new Foo;
+
+$bar = $foo->method(new stdClass, ...);
+
+$bar();
+?>
+--EXPECT--
+Foo::method
diff --git a/Zend/tests/partial_application/variation_strict_001.phpt b/Zend/tests/partial_application/variation_strict_001.phpt
new file mode 100644
index 00000000000..835f6e13805
--- /dev/null
+++ b/Zend/tests/partial_application/variation_strict_001.phpt
@@ -0,0 +1,20 @@
+--TEST--
+PFA variation: strict_types declared
+--FILE--
+<?php
+declare(strict_types=1);
+
+function foo(int $int) {
+    var_dump($int);
+}
+
+$foo = foo(?);
+
+try {
+    $foo("42");
+} catch (TypeError $ex) {
+    printf("%s: %s\n", $ex::class, $ex->getMessage());
+}
+?>
+--EXPECTF--
+TypeError: {closure:%s:%d}(): Argument #1 ($int) must be of type int, string given, called in %s on line %d
diff --git a/Zend/tests/partial_application/variation_variadics_001.phpt b/Zend/tests/partial_application/variation_variadics_001.phpt
new file mode 100644
index 00000000000..850f0eda149
--- /dev/null
+++ b/Zend/tests/partial_application/variation_variadics_001.phpt
@@ -0,0 +1,31 @@
+--TEST--
+PFA variation: variadics, user function
+--FILE--
+<?php
+function foo($a, ...$b) {
+    var_dump($a, ...$b);
+}
+
+$foo = foo(10, 100, ...);
+
+echo (string) new ReflectionFunction($foo);
+
+$foo(1000, 10000);
+?>
+--EXPECTF--
+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 ]
+  }
+}
+int(10)
+int(100)
+int(1000)
+int(10000)
diff --git a/Zend/tests/partial_application/variation_variadics_002.phpt b/Zend/tests/partial_application/variation_variadics_002.phpt
new file mode 100644
index 00000000000..21d8169fc42
--- /dev/null
+++ b/Zend/tests/partial_application/variation_variadics_002.phpt
@@ -0,0 +1,25 @@
+--TEST--
+PFA variation: variadics, internal function
+--FILE--
+<?php
+$sprintf = sprintf("%d %d %d", 100, ...);
+
+echo (string) new ReflectionFunction($sprintf);
+
+echo $sprintf(1000, 10000);
+?>
+--EXPECTF--
+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 ]
+  }
+  - Return [ string ]
+}
+100 1000 10000
diff --git a/Zend/tests/partial_application/variation_variadics_004.phpt b/Zend/tests/partial_application/variation_variadics_004.phpt
new file mode 100644
index 00000000000..228404dc827
--- /dev/null
+++ b/Zend/tests/partial_application/variation_variadics_004.phpt
@@ -0,0 +1,45 @@
+--TEST--
+PFA variation: variadics and optional args
+--FILE--
+<?php
+function foo(int $day = 1, int $month = 1, int $year = 2005) {
+    printf("%04d-%02d-%02d\n", $year, $month, $day);
+}
+
+$foo = foo(year: 2006, ...);
+
+echo "# Bound year, pass day:\n";
+
+$foo(2);
+
+$foo = foo(month: 12, ...);
+
+$bar = $foo(year: 2016, ...);
+
+echo "# Bound month, pass day:\n";
+
+$foo(2);
+
+echo "# Bound month, bound year, pass day:\n";
+
+$bar(2);
+
+echo "# Bound month, no args:\n";
+
+$foo();
+
+echo "# Bound month, bound year, no args:\n";
+
+$bar();
+?>
+--EXPECT--
+# Bound year, pass day:
+2006-01-02
+# Bound month, pass day:
+2005-12-02
+# Bound month, bound year, pass day:
+2016-12-02
+# Bound month, no args:
+2005-12-01
+# Bound month, bound year, no args:
+2016-12-01
diff --git a/Zend/tests/partial_application/variation_variadics_006.phpt b/Zend/tests/partial_application/variation_variadics_006.phpt
new file mode 100644
index 00000000000..cda62a2f3bf
--- /dev/null
+++ b/Zend/tests/partial_application/variation_variadics_006.phpt
@@ -0,0 +1,17 @@
+--TEST--
+PFA variation: named may overwrite variadic placeholder
+--FILE--
+<?php
+function foo($a) {
+    var_dump(func_get_args());
+}
+
+$foo = foo(a: "a", ...);
+
+$foo();
+?>
+--EXPECTF--
+array(1) {
+  [0]=>
+  string(1) "a"
+}
diff --git a/Zend/tests/partial_application/variation_variadics_007.phpt b/Zend/tests/partial_application/variation_variadics_007.phpt
new file mode 100644
index 00000000000..9624e05b449
--- /dev/null
+++ b/Zend/tests/partial_application/variation_variadics_007.phpt
@@ -0,0 +1,14 @@
+--TEST--
+PFA variation: extra through variadic
+--FILE--
+<?php
+function appliesCb($cb) { return runsCb($cb(1, ?, ...)); }
+
+function runsCb($cb) {
+        return $cb(2, 3);
+}
+
+var_dump(appliesCb(fn($a, $b) => $a + $b));
+?>
+--EXPECT--
+int(3)
diff --git a/Zend/tests/partial_application/variation_variadics_008.phpt b/Zend/tests/partial_application/variation_variadics_008.phpt
new file mode 100644
index 00000000000..4190bb759e6
--- /dev/null
+++ b/Zend/tests/partial_application/variation_variadics_008.phpt
@@ -0,0 +1,16 @@
+--TEST--
+PFA variation: variadics wrong signature checked
+--FILE--
+<?php
+function foo(...$c) {}
+
+$fn = foo(?);
+
+try {
+    $fn(?, ?);
+} catch (Error $ex) {
+    echo $ex->getMessage() . PHP_EOL;
+}
+?>
+--EXPECTF--
+Partial application of {closure:%s:%d}() expects at most 1 arguments, 2 given
diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c
index 3aa04de860a..adcb62a51d1 100644
--- a/Zend/zend_ast.c
+++ b/Zend/zend_ast.c
@@ -1459,6 +1459,16 @@ ZEND_API zend_ast_ref * ZEND_FASTCALL zend_ast_copy(zend_ast *ast)
 	return ref;
 }

+ZEND_API zend_ast * ZEND_FASTCALL zend_ast_dup(zend_ast *ast)
+{
+	ZEND_ASSERT(ast != NULL);
+
+	zend_ast *buf = zend_ast_alloc(zend_ast_tree_size(ast));
+	zend_ast_tree_copy(ast, buf);
+
+	return buf;
+}
+
 ZEND_API void ZEND_FASTCALL zend_ast_destroy(zend_ast *ast)
 {
 tail_call:
diff --git a/Zend/zend_ast.h b/Zend/zend_ast.h
index 86a68c1cbaf..26f54102a14 100644
--- a/Zend/zend_ast.h
+++ b/Zend/zend_ast.h
@@ -350,7 +350,10 @@ ZEND_API zend_result ZEND_FASTCALL zend_ast_evaluate(zval *result, zend_ast *ast
 ZEND_API zend_result ZEND_FASTCALL zend_ast_evaluate_ex(zval *result, zend_ast *ast, zend_class_entry *scope, bool *short_circuited_ptr, zend_ast_evaluate_ctx *ctx);
 ZEND_API zend_string *zend_ast_export(const char *prefix, zend_ast *ast, const char *suffix);

+/* Copies 'ast' to the heap, returns a refcounted AST reference */
 ZEND_API zend_ast_ref * ZEND_FASTCALL zend_ast_copy(zend_ast *ast);
+/* Duplicates 'ast' on the arena */
+ZEND_API zend_ast * ZEND_FASTCALL zend_ast_dup(zend_ast *ast);
 ZEND_API void ZEND_FASTCALL zend_ast_destroy(zend_ast *ast);
 ZEND_API void ZEND_FASTCALL zend_ast_ref_destroy(zend_ast_ref *ast);

diff --git a/Zend/zend_closures.c b/Zend/zend_closures.c
index 840d2dbe32e..91f690c73ed 100644
--- a/Zend/zend_closures.c
+++ b/Zend/zend_closures.c
@@ -27,6 +27,11 @@
 #include "zend_globals.h"
 #include "zend_closures_arginfo.h"

+/* Closure is a PFA */
+#define ZEND_PARTIAL            OBJ_EXTRA_FLAG_PRIV_1
+/* Closure is a PFA of a Closure. Rebinding the PFA requires rebinding the inner Closure. */
+#define ZEND_PARTIAL_OF_CLOSURE OBJ_EXTRA_FLAG_PRIV_2
+
 typedef struct _zend_closure {
 	zend_object       std;
 	zend_function     func;
@@ -40,6 +45,17 @@ ZEND_API zend_class_entry *zend_ce_closure;
 static zend_object_handlers closure_handlers;

 static zend_result zend_closure_get_closure(zend_object *obj, zend_class_entry **ce_ptr, zend_function **fptr_ptr, zend_object **obj_ptr, bool check_only);
+static void zend_create_closure_ex(zval *res, zend_function *func, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr, bool is_fake, uint32_t flags);
+
+static inline uint32_t zend_closure_flags(const zend_closure *closure)
+{
+	return closure->std.extra_flags & (ZEND_PARTIAL|ZEND_PARTIAL_OF_CLOSURE);
+}
+
+static inline bool zend_closure_is_fake(const zend_closure *closure)
+{
+	return closure->func.common.fn_flags & ZEND_ACC_FAKE_CLOSURE;
+}

 ZEND_METHOD(Closure, __invoke) /* {{{ */
 {
@@ -77,7 +93,9 @@ static bool zend_valid_closure_binding(
 		zend_closure *closure, zval *newthis, zend_class_entry *scope) /* {{{ */
 {
 	zend_function *func = &closure->func;
-	bool is_fake_closure = (func->common.fn_flags & ZEND_ACC_FAKE_CLOSURE) != 0;
+	// TODO: rename variable
+	bool is_fake_closure = (func->common.fn_flags & ZEND_ACC_FAKE_CLOSURE) != 0
+		|| (closure->std.extra_flags & ZEND_PARTIAL);
 	if (newthis) {
 		if (func->common.fn_flags & ZEND_ACC_STATIC) {
 			zend_error(E_WARNING, "Cannot bind an instance to a static closure, this will be an error in PHP 9");
@@ -161,7 +179,9 @@ ZEND_METHOD(Closure, call)

 	if (closure->func.common.fn_flags & ZEND_ACC_GENERATOR) {
 		zval new_closure;
-		zend_create_closure(&new_closure, &closure->func, newclass, closure->called_scope, newthis);
+		zend_create_closure_ex(&new_closure, &closure->func, newclass,
+				closure->called_scope, newthis,
+				zend_closure_is_fake(closure), zend_closure_flags(closure));
 		closure = (zend_closure *) Z_OBJ(new_closure);
 		fci_cache.function_handler = &closure->func;

@@ -177,6 +197,7 @@ ZEND_METHOD(Closure, call)
 		memset(&fake_closure->std, 0, sizeof(fake_closure->std));
 		fake_closure->std.gc.refcount = 1;
 		fake_closure->std.gc.u.type_info = GC_NULL;
+		fake_closure->std.extra_flags = zend_closure_flags(closure);
 		ZVAL_UNDEF(&fake_closure->this_ptr);
 		fake_closure->called_scope = NULL;
 		my_function = &fake_closure->func;
@@ -223,7 +244,7 @@ ZEND_METHOD(Closure, call)
 }
 /* }}} */

-static void do_closure_bind(zval *return_value, zval *zclosure, zval *newthis, zend_object *scope_obj, zend_string *scope_str)
+static zend_result do_closure_bind(zval *return_value, zval *zclosure, zval *newthis, zend_object *scope_obj, zend_string *scope_str)
 {
 	zend_class_entry *ce, *called_scope;
 	zend_closure *closure = (zend_closure *) Z_OBJ_P(zclosure);
@@ -235,14 +256,15 @@ static void do_closure_bind(zval *return_value, zval *zclosure, zval *newthis, z
 			ce = closure->func.common.scope;
 		} else if ((ce = zend_lookup_class(scope_str)) == NULL) {
 			zend_error(E_WARNING, "Class \"%s\" not found", ZSTR_VAL(scope_str));
-			RETURN_NULL();
+			RETVAL_NULL();
+			return FAILURE;
 		}
 	} else {
 		ce = NULL;
 	}

 	if (!zend_valid_closure_binding(closure, newthis, ce)) {
-		return;
+		return FAILURE;
 	}

 	if (newthis) {
@@ -251,7 +273,34 @@ static void do_closure_bind(zval *return_value, zval *zclosure, zval *newthis, z
 		called_scope = ce;
 	}

-	zend_create_closure(return_value, &closure->func, ce, called_scope, newthis);
+	zend_create_closure_ex(return_value, &closure->func, ce, called_scope, newthis,
+		zend_closure_is_fake(closure), zend_closure_flags(closure));
+
+	if (zend_closure_flags(closure) & ZEND_PARTIAL_OF_CLOSURE) {
+		/* Re-bind the inner closure */
+
+		zend_closure *new_closure = (zend_closure*)Z_OBJ_P(return_value);
+		HashTable *static_variables = ZEND_MAP_PTR_GET(new_closure->func.op_array.static_variables_ptr);
+		ZEND_ASSERT(static_variables->nNumOfElements > 0);
+		zval *inner = &static_variables->arData[0].val;
+		ZEND_ASSERT(Z_TYPE_P(inner) == IS_OBJECT && Z_OBJCE_P(inner) == zend_ce_closure);
+
+		zval new_inner;
+		if (do_closure_bind(&new_inner, inner, newthis, scope_obj, scope_str) != SUCCESS) {
+			/* Should not happen, as we have already validated arguments and the
+			 * inner closure should have the same constraints. */
+			ZEND_UNREACHABLE();
+			zval_ptr_dtor(return_value);
+			ZVAL_NULL(return_value);
+			return FAILURE;
+		}
+
+		zend_object *garbage = Z_OBJ_P(inner);
+		ZVAL_COPY_VALUE(inner, &new_inner);
+		zend_object_release(garbage);
+	}
+
+	return SUCCESS;
 }

 /* {{{ Create a closure from another one and bind to another object and scope */
@@ -588,8 +637,9 @@ static zend_object *zend_closure_clone(zend_object *zobject) /* {{{ */
 	zend_closure *closure = (zend_closure *)zobject;
 	zval result;

-	zend_create_closure(&result, &closure->func,
-		closure->func.common.scope, closure->called_scope, &closure->this_ptr);
+	zend_create_closure_ex(&result, &closure->func,
+		closure->func.common.scope, closure->called_scope, &closure->this_ptr,
+		zend_closure_is_fake(closure), zend_closure_flags(closure));
 	return Z_OBJ(result);
 }
 /* }}} */
@@ -757,7 +807,7 @@ static ZEND_NAMED_FUNCTION(zend_closure_internal_handler) /* {{{ */
 }
 /* }}} */

-static void zend_create_closure_ex(zval *res, zend_function *func, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr, bool is_fake) /* {{{ */
+static void zend_create_closure_ex(zval *res, zend_function *func, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr, bool is_fake, uint32_t flags) /* {{{ */
 {
 	zend_closure *closure;
 	void *ptr;
@@ -765,6 +815,7 @@ static void zend_create_closure_ex(zval *res, zend_function *func, zend_class_en
 	object_init_ex(res, zend_ce_closure);

 	closure = (zend_closure *)Z_OBJ_P(res);
+	closure->std.extra_flags = flags;

 	if ((scope == NULL) && this_ptr && (Z_TYPE_P(this_ptr) != IS_UNDEF)) {
 		/* use dummy scope if we're binding an object without specifying a scope */
@@ -862,14 +913,16 @@ static void zend_create_closure_ex(zval *res, zend_function *func, zend_class_en
 ZEND_API void zend_create_closure(zval *res, zend_function *func, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr)
 {
 	zend_create_closure_ex(res, func, scope, called_scope, this_ptr,
-		/* is_fake */ (func->common.fn_flags & ZEND_ACC_FAKE_CLOSURE) != 0);
+		/* is_fake */ (func->common.fn_flags & ZEND_ACC_FAKE_CLOSURE) != 0,
+		/* flags */ 0);
 }

 ZEND_API void zend_create_fake_closure(zval *res, zend_function *func, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr) /* {{{ */
 {
 	zend_closure *closure;

-	zend_create_closure_ex(res, func, scope, called_scope, this_ptr, /* is_fake */ true);
+	zend_create_closure_ex(res, func, scope, called_scope, this_ptr,
+			/* is_fake */ true, /* flags */ 0);

 	closure = (zend_closure *)Z_OBJ_P(res);
 	closure->func.common.fn_flags |= ZEND_ACC_FAKE_CLOSURE;
@@ -879,6 +932,16 @@ ZEND_API void zend_create_fake_closure(zval *res, zend_function *func, zend_clas
 }
 /* }}} */

+ZEND_API void zend_create_partial_closure(zval *res, zend_function *func, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr, bool partial_of_closure)
+{
+	uint32_t flags = ZEND_PARTIAL;
+	if (partial_of_closure) {
+		flags |= ZEND_PARTIAL_OF_CLOSURE;
+	}
+	zend_create_closure_ex(res, func, scope, called_scope, this_ptr,
+			/* is_fake */ false, flags);
+}
+
 void zend_closure_from_frame(zval *return_value, const zend_execute_data *call) { /* {{{ */
 	zval instance;
 	zend_internal_function trampoline;
diff --git a/Zend/zend_closures.h b/Zend/zend_closures.h
index 2ff4934f2a3..305d82e5015 100644
--- a/Zend/zend_closures.h
+++ b/Zend/zend_closures.h
@@ -36,6 +36,7 @@ extern ZEND_API zend_class_entry *zend_ce_closure;

 ZEND_API void zend_create_closure(zval *res, zend_function *op_array, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr);
 ZEND_API void zend_create_fake_closure(zval *res, zend_function *op_array, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr);
+ZEND_API void zend_create_partial_closure(zval *res, zend_function *func, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr, bool partial_of_closure);
 ZEND_API zend_function *zend_get_closure_invoke_method(zend_object *obj);
 ZEND_API const zend_function *zend_get_closure_method_def(zend_object *obj);
 ZEND_API zval* zend_get_closure_this_ptr(zval *obj);
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index ac5a9d71a6e..f4478423953 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -28,6 +28,8 @@
 #include "zend_exceptions.h"
 #include "zend_interfaces.h"
 #include "zend_types.h"
+#include "zend_portability.h"
+#include "zend_string.h"
 #include "zend_virtual_cwd.h"
 #include "zend_multibyte.h"
 #include "zend_language_scanner.h"
@@ -102,6 +104,8 @@ static void zend_compile_expr(znode *result, zend_ast *ast);
 static void zend_compile_stmt(zend_ast *ast);
 static void zend_compile_assign(znode *result, zend_ast *ast, bool stmt, uint32_t type);

+static zend_ast *zend_partial_apply(zend_ast *callable_ast, zend_ast *pipe_arg);
+
 #ifdef ZEND_CHECK_STACK_LIMIT
 zend_never_inline static void zend_stack_limit_error(void)
 {
@@ -3771,12 +3775,16 @@ static uint32_t zend_get_arg_num(const zend_function *fn, const zend_string *arg
 	return (uint32_t) -1;
 }

-static uint32_t zend_compile_args(
-		zend_ast *ast, const zend_function *fbc, bool *may_have_extra_named_args) /* {{{ */
+static uint32_t zend_compile_args_ex(
+		zend_ast *ast, const zend_function *fbc,
+		bool *may_have_extra_named_args,
+		bool is_call_partial, bool *uses_variadic_placeholder_p,
+		zval *named_positions) /* {{{ */
 {
 	const zend_ast_list *args = zend_ast_get_list(ast);
 	uint32_t i;
 	bool uses_arg_unpack = false;
+	bool uses_variadic_placeholder = false;
 	uint32_t arg_count = 0; /* number of arguments not including unpacks */

 	/* Whether named arguments are used syntactically, to enforce language level limitations.
@@ -3802,6 +3810,11 @@ static uint32_t zend_compile_args(
 					"Cannot use argument unpacking after named arguments");
 			}

+			if (is_call_partial) {
+				zend_error_noreturn(E_COMPILE_ERROR,
+					"Cannot combine partial application and unpacking");
+			}
+
 			/* Unpack may contain named arguments. */
 			may_have_undef = true;
 			if (!fbc || (fbc->common.fn_flags & ZEND_ACC_VARIADIC)) {
@@ -3842,18 +3855,75 @@ static uint32_t zend_compile_args(
 				may_have_undef = true;
 				*may_have_extra_named_args = true;
 			}
+
+			if (uses_variadic_placeholder) {
+				zend_error_noreturn(E_COMPILE_ERROR,
+					"Variadic placeholder must be last");
+			}
 		} else {
 			if (uses_arg_unpack) {
 				zend_error_noreturn(E_COMPILE_ERROR,
 					"Cannot use positional argument after argument unpacking");
 			}

-			if (uses_named_args) {
+			bool is_variadic_placeholder = arg->kind == ZEND_AST_PLACEHOLDER_ARG
+				&& arg->attr == ZEND_PLACEHOLDER_VARIADIC;
+
+			if (uses_named_args && !is_variadic_placeholder) {
 				zend_error_noreturn(E_COMPILE_ERROR,
 					"Cannot use positional argument after named argument");
 			}

-			arg_count++;
+			if (uses_variadic_placeholder) {
+				if (is_variadic_placeholder) {
+					zend_error_noreturn(E_COMPILE_ERROR,
+						"Variadic placeholder may only appear once");
+				} else {
+					zend_error_noreturn(E_COMPILE_ERROR,
+						"Variadic placeholder must be last");
+				}
+			}
+
+			if (!is_variadic_placeholder) {
+				arg_count++;
+			}
+		}
+
+		if (arg->kind == ZEND_AST_PLACEHOLDER_ARG) {
+			if (uses_arg_unpack) {
+				zend_error_noreturn(E_COMPILE_ERROR,
+					"Cannot combine partial application and unpacking");
+			}
+
+			if (arg->attr == ZEND_PLACEHOLDER_VARIADIC) {
+				uses_variadic_placeholder = true;
+				/* Do not emit ZEND_SEND_PLACEHOLDER: We represent the variadic
+				 * placeholder with a flag on the ZEND_CONVERT_CALLABLE_PARTIAL
+				 * op instead. */
+				continue;
+			}
+
+			if (arg_name) {
+				if (Z_ISUNDEF_P(named_positions)) {
+					array_init(named_positions);
+				}
+				zval tmp;
+				ZVAL_LONG(&tmp, zend_hash_num_elements(Z_ARRVAL_P(named_positions)));
+				zend_hash_add(Z_ARRVAL_P(named_positions), arg_name, &tmp);
+			}
+
+			opline = zend_emit_op(NULL, ZEND_SEND_PLACEHOLDER, NULL, NULL);
+			if (arg_name) {
+				opline->op2_type = IS_CONST;
+				zend_string_addref(arg_name);
+				opline->op2.constant = zend_add_literal_string(&arg_name);
+				opline->result.num = zend_alloc_cache_slots(2);
+			} else if (arg->attr != ZEND_PLACEHOLDER_VARIADIC) {
+				opline->op2.opline_num = arg_num;
+				opline->result.var = EX_NUM_TO_VAR(arg_num - 1);
+			}
+
+			continue;
 		}

 		/* Treat passing of $GLOBALS the same as passing a call.
@@ -3968,14 +4038,23 @@ static uint32_t zend_compile_args(
 		}
 	}

-	if (may_have_undef) {
-		zend_emit_op(NULL, ZEND_CHECK_UNDEF_ARGS, NULL, NULL);
+	if (!is_call_partial) {
+		if (may_have_undef) {
+			zend_emit_op(NULL, ZEND_CHECK_UNDEF_ARGS, NULL, NULL);
+		}
+	} else {
+		*uses_variadic_placeholder_p = uses_variadic_placeholder;
 	}

 	return arg_count;
 }
 /* }}} */

+static uint32_t zend_compile_args(zend_ast *ast, const zend_function *fbc, bool *may_have_extra_named_args)
+{
+	return zend_compile_args_ex(ast, fbc, may_have_extra_named_args, false, NULL, NULL);
+}
+
 ZEND_API uint8_t zend_get_call_op(const zend_op *init_op, const zend_function *fbc, bool result_used) /* {{{ */
 {
 	uint32_t no_discard = result_used ? 0 : ZEND_ACC_NODISCARD;
@@ -4009,6 +4088,38 @@ ZEND_API uint8_t zend_get_call_op(const zend_op *init_op, const zend_function *f
 }
 /* }}} */

+static void zend_compile_call_partial(znode *result, uint32_t arg_count,
+		bool may_have_extra_named_args, bool uses_variadic_placeholder,
+		zval *named_positions, uint32_t opnum_init, const zend_function *fbc) {
+
+	zend_op *init_opline = &CG(active_op_array)->opcodes[opnum_init];
+
+	init_opline->extended_value = arg_count;
+
+	ZEND_ASSERT(init_opline->opcode != ZEND_NEW);
+
+	if (init_opline->opcode == ZEND_INIT_FCALL) {
+		init_opline->op1.num = zend_vm_calc_used_stack(arg_count, fbc);
+	}
+
+	zend_op *opline = zend_emit_op_tmp(result, ZEND_CALLABLE_CONVERT_PARTIAL,
+				NULL, NULL);
+
+	opline->op1.num = zend_alloc_cache_slots(2);
+
+	if (may_have_extra_named_args) {
+		opline->extended_value = ZEND_FCALL_MAY_HAVE_EXTRA_NAMED_PARAMS;
+	}
+	if (uses_variadic_placeholder) {
+		opline->extended_value |= ZEND_FCALL_USES_VARIADIC_PLACEHOLDER;
+	}
+
+	if (!Z_ISUNDEF_P(named_positions)) {
+		opline->op2.constant = zend_add_literal(named_positions);
+		opline->op2_type = IS_CONST;
+	}
+}
+
 static bool zend_compile_call_common(znode *result, zend_ast *args_ast, const zend_function *fbc, uint32_t lineno, uint32_t type) /* {{{ */
 {
 	zend_op *opline;
@@ -4024,23 +4135,43 @@ static bool zend_compile_call_common(znode *result, zend_ast *args_ast, const ze
 			zend_error_noreturn(E_COMPILE_ERROR, "Cannot create Closure for new expression");
 		}

-		zend_ast_list *args = zend_ast_get_list(((zend_ast_fcc*)args_ast)->args);
-		if (args->children != 1 || args->child[0]->attr != ZEND_PLACEHOLDER_VARIADIC) {
-			zend_error_noreturn(E_COMPILE_ERROR, "Cannot create a Closure for call expression with more than one argument, or non-variadic placeholders");
-		}
+		zend_ast_list *fcc_args = zend_ast_get_list(((zend_ast_fcc*)args_ast)->args);

-		if (opcode == ZEND_INIT_FCALL) {
-			opline->op1.num = zend_vm_calc_used_stack(0, fbc);
-		}
+		/* FCCs are a special case of PFAs with a single variadic placeholder */
+		if (fcc_args->children == 1 && fcc_args->child[0]->attr == ZEND_PLACEHOLDER_VARIADIC) {

-		zend_op *callable_convert_op = zend_emit_op_tmp(result, ZEND_CALLABLE_CONVERT, NULL, NULL);
-		if (opcode == ZEND_INIT_FCALL
-		 || opcode == ZEND_INIT_FCALL_BY_NAME
-		 || opcode == ZEND_INIT_NS_FCALL_BY_NAME) {
-			callable_convert_op->extended_value = zend_alloc_cache_slot();
-		} else {
-			callable_convert_op->extended_value = (uint32_t)-1;
+			if (opline->opcode == ZEND_INIT_FCALL) {
+				opline->op1.num = zend_vm_calc_used_stack(0, fbc);
+			}
+
+			zend_op *callable_convert_op = zend_emit_op_tmp(result, ZEND_CALLABLE_CONVERT, NULL, NULL);
+			if (opcode == ZEND_INIT_FCALL
+			 || opcode == ZEND_INIT_FCALL_BY_NAME
+			 || opcode == ZEND_INIT_NS_FCALL_BY_NAME) {
+				callable_convert_op->extended_value = zend_alloc_cache_slot();
+			} else {
+				callable_convert_op->extended_value = (uint32_t)-1;
+			}
+
+			return true;
 		}
+
+		args_ast = ((zend_ast_fcc*)args_ast)->args;
+
+		bool may_have_extra_named_args;
+		bool uses_variadic_placeholder;
+
+		zval named_positions;
+		ZVAL_UNDEF(&named_positions);
+
+		uint32_t arg_count = zend_compile_args_ex(args_ast, fbc,
+				&may_have_extra_named_args, true, &uses_variadic_placeholder,
+				&named_positions);
+
+		zend_compile_call_partial(result, arg_count,
+				may_have_extra_named_args, uses_variadic_placeholder,
+				&named_positions, opnum_init, fbc);
+
 		return true;
 	}

@@ -5126,42 +5257,21 @@ static zend_result zend_compile_func_array_map(znode *result, zend_ast_list *arg
 	}

 	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
-	 && callback->child[0]->kind == ZEND_AST_ZVAL
-	 && Z_TYPE_P(zend_ast_get_zval(callback->child[0])) == IS_STRING
-	 && zend_string_equals_literal_ci(zend_ast_get_str(callback->child[0]), "assert")) {
-		return FAILURE;
-	}
-
-	zend_ast_list *callback_args = zend_ast_get_list(((zend_ast_fcc*)args_ast)->args);
-	if (callback_args->children != 1 || callback_args->child[0]->attr != ZEND_PLACEHOLDER_VARIADIC) {
-		/* Full PFA is not yet implemented, will fail in zend_compile_call_common(). */
+	if (callback->kind != ZEND_AST_CALL && callback->kind != ZEND_AST_STATIC_CALL) {
 		return FAILURE;
 	}

 	znode value;
 	value.op_type = IS_TMP_VAR;
 	value.u.op.var = get_temporary_variable();
-	zend_ast *call_args = zend_ast_create_list(1, ZEND_AST_ARG_LIST, zend_ast_create_znode(&value));
+
+	zend_ast *call_args = zend_partial_apply(callback,
+			zend_ast_create_znode(&value));
+	if (!call_args) {
+		CG(active_op_array)->T--;
+		/* The callback is not a FCC/PFA, or is not optimizable */
+		return FAILURE;
+	}

 	zend_op *opline;

@@ -6769,6 +6879,76 @@ static bool zend_is_pipe_optimizable_callable_name(zend_ast *ast)
 	return true;
 }

+static zend_ast *zend_partial_apply(zend_ast *callable_ast, zend_ast *pipe_arg)
+{
+	if (callable_ast->kind != ZEND_AST_CALL
+			&& callable_ast->kind != ZEND_AST_STATIC_CALL
+			&& callable_ast->kind != ZEND_AST_METHOD_CALL) {
+		return NULL;
+	}
+
+	zend_ast *args_ast = zend_ast_call_get_args(callable_ast);
+	if (!args_ast || args_ast->kind != ZEND_AST_CALLABLE_CONVERT) {
+		return NULL;
+	}
+
+	if (callable_ast->kind == ZEND_AST_CALL &&
+			!zend_is_pipe_optimizable_callable_name(callable_ast->child[0])) {
+		return NULL;
+	}
+
+	zend_ast_list *arg_list = zend_ast_get_list(((zend_ast_fcc*)args_ast)->args);
+
+	zend_ast *first_placeholder = NULL;
+	bool uses_named_args = false;
+
+	for (uint32_t i = 0; i < arg_list->children; i++) {
+		zend_ast *arg = arg_list->child[i];
+		if (arg->kind == ZEND_AST_NAMED_ARG) {
+			uses_named_args = true;
+			arg = arg->child[1];
+		}
+
+		if (arg->kind == ZEND_AST_PLACEHOLDER_ARG) {
+			if (first_placeholder == NULL) {
+				first_placeholder = arg;
+			} else {
+				/* A PFA with multiple placeholders is unexpected in this
+				 * context, and will usually error due to a missing argument,
+				 * so we don't optimize those. */
+				return NULL;
+			}
+			if (arg->attr == ZEND_PLACEHOLDER_VARIADIC && uses_named_args) {
+				/* A PFA with both a variadic placeholder and named args can not
+				 * be optimized because this would result in a positional arg
+				 * after a named arg: f(name: $v, ...) -> f(name: $v, pipe_arg).
+				 * Arg placeholders ('?') are safe since they are not allowed
+				 * after named args. */
+				return NULL;
+			}
+		}
+	}
+
+	ZEND_ASSERT(first_placeholder);
+
+	zend_ast *new_arg_list = zend_ast_create_list(0, arg_list->kind);
+	for (uint32_t i = 0; i < arg_list->children; i++) {
+		zend_ast *arg = arg_list->child[i];
+		if (arg == first_placeholder) {
+			new_arg_list = zend_ast_list_add(new_arg_list, pipe_arg);
+		} else if (arg->kind == ZEND_AST_NAMED_ARG
+				&& arg->child[1] == first_placeholder) {
+			zend_ast *name = arg->child[0];
+			new_arg_list = zend_ast_list_add(new_arg_list,
+					zend_ast_create(ZEND_AST_NAMED_ARG, name, pipe_arg));
+		} else {
+			new_arg_list = zend_ast_list_add(new_arg_list, arg);
+		}
+	}
+
+	return new_arg_list;
+}
+
 static void zend_compile_pipe(znode *result, zend_ast *ast, uint32_t type)
 {
 	zend_ast *operand_ast = ast->child[0];
@@ -6793,29 +6973,35 @@ static void zend_compile_pipe(znode *result, zend_ast *ast, uint32_t type)
 	}

 	/* Turn the operand into a function parameter list. */
-	zend_ast *arg_list_ast = zend_ast_create_list(1, ZEND_AST_ARG_LIST, zend_ast_create_znode(&wrapped_operand_result));
+	zend_ast *arg = zend_ast_create_znode(&wrapped_operand_result);

 	zend_ast *fcall_ast;
 	znode callable_result;
+	zend_ast *pfa_arg_list_ast = NULL;

-	/* Turn $foo |> bar(...) into bar($foo). */
-	if (callable_ast->kind == ZEND_AST_CALL
-		&& callable_ast->child[1]->kind == ZEND_AST_CALLABLE_CONVERT
-		&& zend_is_pipe_optimizable_callable_name(callable_ast->child[0])) {
-		fcall_ast = zend_ast_create(ZEND_AST_CALL,
-				callable_ast->child[0], arg_list_ast);
-	/* Turn $foo |> bar::baz(...) into bar::baz($foo). */
-	} else if (callable_ast->kind == ZEND_AST_STATIC_CALL
-			&& callable_ast->child[2]->kind == ZEND_AST_CALLABLE_CONVERT) {
-		fcall_ast = zend_ast_create(ZEND_AST_STATIC_CALL,
-			callable_ast->child[0], callable_ast->child[1], arg_list_ast);
-	/* Turn $foo |> $bar->baz(...) into $bar->baz($foo). */
-	} else if (callable_ast->kind == ZEND_AST_METHOD_CALL
-			&& callable_ast->child[2]->kind == ZEND_AST_CALLABLE_CONVERT) {
-		fcall_ast = zend_ast_create(ZEND_AST_METHOD_CALL,
-			callable_ast->child[0], callable_ast->child[1], arg_list_ast);
+	/* Turn $foo |> PFA into plain function call if possible */
+	if ((pfa_arg_list_ast = zend_partial_apply(callable_ast, arg))) {
+		switch (callable_ast->kind) {
+			case ZEND_AST_CALL:
+				fcall_ast = zend_ast_create(ZEND_AST_CALL,
+						callable_ast->child[0], pfa_arg_list_ast);
+				break;
+			case ZEND_AST_STATIC_CALL:
+				fcall_ast = zend_ast_create(ZEND_AST_STATIC_CALL,
+						callable_ast->child[0], callable_ast->child[1],
+						pfa_arg_list_ast);
+				break;
+			case ZEND_AST_METHOD_CALL:
+				fcall_ast = zend_ast_create(ZEND_AST_METHOD_CALL,
+						callable_ast->child[0], callable_ast->child[1],
+						pfa_arg_list_ast);
+				break;
+			default:
+				ZEND_UNREACHABLE();
+		}
 	/* Turn $foo |> $expr into ($expr)($foo) */
 	} else {
+		zend_ast *arg_list_ast = zend_ast_create_list(1, ZEND_AST_ARG_LIST, arg);
 		zend_compile_expr(&callable_result, callable_ast);
 		callable_ast = zend_ast_create_znode(&callable_result);
 		fcall_ast = zend_ast_create(ZEND_AST_CALL,
@@ -11834,6 +12020,13 @@ static void zend_compile_const_expr_fcc(zend_ast **ast_ptr)
 	if ((*args_ast)->kind != ZEND_AST_CALLABLE_CONVERT) {
 		zend_error_noreturn(E_COMPILE_ERROR, "Constant expression contains invalid operations");
 	}
+
+	zend_ast_list *args = zend_ast_get_list(((zend_ast_fcc*)*args_ast)->args);
+	if (args->children != 1 || args->child[0]->attr != ZEND_PLACEHOLDER_VARIADIC) {
+		// TODO: PFAs
+		zend_error_noreturn(E_COMPILE_ERROR, "Constant expression contains invalid operations");
+	}
+
 	ZEND_MAP_PTR_NEW(((zend_ast_fcc *)*args_ast)->fptr);

 	switch ((*ast_ptr)->kind) {
diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h
index 2351882a560..69d7aeb2f37 100644
--- a/Zend/zend_compile.h
+++ b/Zend/zend_compile.h
@@ -958,6 +958,7 @@ struct _zend_arena;
 ZEND_API zend_op_array *compile_file(zend_file_handle *file_handle, int type);
 ZEND_API zend_op_array *compile_string(zend_string *source_string, const char *filename, zend_compile_position position);
 ZEND_API zend_op_array *compile_filename(int type, zend_string *filename);
+ZEND_API zend_op_array *zend_compile_ast(zend_ast *ast, int type, zend_string *filename);
 ZEND_API zend_ast *zend_compile_string_to_ast(
 		zend_string *code, struct _zend_arena **ast_arena, zend_string *filename);
 ZEND_API zend_result zend_execute_scripts(int type, zval *retval, int file_count, ...);
@@ -1119,7 +1120,8 @@ ZEND_API zend_string *zend_type_to_string(zend_type type);

 #define ZEND_THROW_IS_EXPR 1u

-#define ZEND_FCALL_MAY_HAVE_EXTRA_NAMED_PARAMS 1
+#define ZEND_FCALL_MAY_HAVE_EXTRA_NAMED_PARAMS (1<<0)
+#define ZEND_FCALL_USES_VARIADIC_PLACEHOLDER   (1<<1)

 /* The send mode, the is_variadic, the is_promoted, and the is_tentative flags are stored as part of zend_type */
 #define _ZEND_SEND_MODE_SHIFT _ZEND_TYPE_EXTRA_FLAGS_SHIFT
diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c
index 1b28ce25fe3..c51a55bac4c 100644
--- a/Zend/zend_execute.c
+++ b/Zend/zend_execute.c
@@ -1232,6 +1232,13 @@ static zend_always_inline bool zend_check_type(
 	return zend_check_type_slow(type, arg, ref, is_return_type, is_internal);
 }

+/* We can not expose zend_check_type() directly because it's inline and uses static functions */
+ZEND_API bool zend_check_type_ex(
+		const zend_type *type, zval *arg, bool is_return_type, bool is_internal)
+{
+	return zend_check_type(type, arg, is_return_type, is_internal);
+}
+
 ZEND_API bool zend_check_user_type_slow(
 		const zend_type *type, zval *arg, const zend_reference *ref, bool is_return_type)
 {
@@ -4676,6 +4683,7 @@ ZEND_API void zend_unfinished_calls_gc(zend_execute_data *execute_data, zend_exe
 				case ZEND_DO_UCALL:
 				case ZEND_DO_FCALL_BY_NAME:
 				case ZEND_CALLABLE_CONVERT:
+				case ZEND_CALLABLE_CONVERT_PARTIAL:
 					level++;
 					break;
 				case ZEND_INIT_FCALL:
@@ -4732,6 +4740,7 @@ ZEND_API void zend_unfinished_calls_gc(zend_execute_data *execute_data, zend_exe
 					case ZEND_DO_UCALL:
 					case ZEND_DO_FCALL_BY_NAME:
 					case ZEND_CALLABLE_CONVERT:
+					case ZEND_CALLABLE_CONVERT_PARTIAL:
 						level++;
 						break;
 					case ZEND_INIT_FCALL:
@@ -4812,6 +4821,7 @@ static void cleanup_unfinished_calls(zend_execute_data *execute_data, uint32_t o
 					case ZEND_DO_UCALL:
 					case ZEND_DO_FCALL_BY_NAME:
 					case ZEND_CALLABLE_CONVERT:
+					case ZEND_CALLABLE_CONVERT_PARTIAL:
 						level++;
 						break;
 					case ZEND_INIT_FCALL:
@@ -4869,6 +4879,7 @@ static void cleanup_unfinished_calls(zend_execute_data *execute_data, uint32_t o
 						case ZEND_DO_UCALL:
 						case ZEND_DO_FCALL_BY_NAME:
 						case ZEND_CALLABLE_CONVERT:
+						case ZEND_CALLABLE_CONVERT_PARTIAL:
 							level++;
 							break;
 						case ZEND_INIT_FCALL:
@@ -5578,9 +5589,10 @@ zval * ZEND_FASTCALL zend_handle_named_arg(
 		}
 	} else {
 		arg = ZEND_CALL_VAR_NUM(call, arg_offset);
+
 		if (UNEXPECTED(!Z_ISUNDEF_P(arg))) {
-			zend_throw_error(NULL, "Named parameter $%s overwrites previous argument",
-				ZSTR_VAL(arg_name));
+			zend_throw_error(NULL, "Named parameter $%s overwrites previous %s",
+				ZSTR_VAL(arg_name), Z_TYPE_P(arg) == _IS_PLACEHOLDER ? "placeholder" : "argument");
 			return NULL;
 		}
 	}
diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h
index ba48b19bcfe..48f7e7a7253 100644
--- a/Zend/zend_execute.h
+++ b/Zend/zend_execute.h
@@ -109,6 +109,8 @@ ZEND_API zend_never_inline ZEND_COLD void zend_verify_never_error(
 ZEND_API bool zend_verify_ref_array_assignable(zend_reference *ref);
 ZEND_API bool zend_check_user_type_slow(
 		const zend_type *type, zval *arg, const zend_reference *ref, bool is_return_type);
+ZEND_API bool zend_check_type_ex(
+		const zend_type *type, zval *arg, bool is_return_type, bool is_internal);

 #if ZEND_DEBUG
 ZEND_API bool zend_internal_call_should_throw(const zend_function *fbc, zend_execute_data *call);
diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c
index cb48e623437..88b0a485750 100644
--- a/Zend/zend_execute_API.c
+++ b/Zend/zend_execute_API.c
@@ -38,6 +38,7 @@
 #include "zend_observer.h"
 #include "zend_call_stack.h"
 #include "zend_frameless_function.h"
+#include "zend_partial.h"
 #ifdef HAVE_SYS_TIME_H
 #include <sys/time.h>
 #endif
@@ -201,6 +202,7 @@ void init_executor(void) /* {{{ */
 	zend_weakrefs_init();

 	zend_hash_init(&EG(callable_convert_cache), 8, NULL, ZVAL_PTR_DTOR, 0);
+	zend_hash_init(&EG(partial_function_application_cache), 8, NULL, zend_partial_op_array_dtor, 0);

 	EG(active) = 1;
 }
@@ -418,6 +420,7 @@ ZEND_API void zend_shutdown_executor_values(bool fast_shutdown)
 		zend_stack_clean(&EG(user_exception_handlers), (void (*)(void *))ZVAL_PTR_DTOR, 1);

 		zend_hash_clean(&EG(callable_convert_cache));
+		zend_hash_clean(&EG(partial_function_application_cache));

 #if ZEND_DEBUG
 		if (!CG(unclean_shutdown)) {
@@ -514,6 +517,7 @@ void shutdown_executor(void) /* {{{ */
 		}

 		zend_hash_destroy(&EG(callable_convert_cache));
+		zend_hash_destroy(&EG(partial_function_application_cache));
 	}

 #if ZEND_DEBUG
diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h
index f78567cfaa7..83360a2c96d 100644
--- a/Zend/zend_globals.h
+++ b/Zend/zend_globals.h
@@ -325,6 +325,7 @@ struct _zend_executor_globals {
 	zend_strtod_state strtod_state;

 	HashTable callable_convert_cache;
+	HashTable partial_function_application_cache;

 	void *reserved[ZEND_MAX_RESERVED_RESOURCES];
 };
diff --git a/Zend/zend_language_scanner.l b/Zend/zend_language_scanner.l
index 1d7e41a2a7d..bf6bbe7f9d9 100644
--- a/Zend/zend_language_scanner.l
+++ b/Zend/zend_language_scanner.l
@@ -594,6 +594,43 @@ ZEND_API zend_result open_file_for_scanning(zend_file_handle *file_handle)
 	return SUCCESS;
 }

+static zend_op_array *zend_compile_ast_internal(int type)
+{
+	ZEND_ASSERT(CG(in_compilation));
+	ZEND_ASSERT(CG(ast));
+
+	uint32_t last_lineno = CG(zend_lineno);
+	zend_file_context original_file_context;
+	zend_oparray_context original_oparray_context;
+	zend_op_array *original_active_op_array = CG(active_op_array);
+
+	zend_op_array *op_array = emalloc(sizeof(*op_array));
+	init_op_array(op_array, type, INITIAL_OP_ARRAY_SIZE);
+	CG(active_op_array) = op_array;
+
+	/* Use heap to not waste arena memory */
+	op_array->fn_flags |= ZEND_ACC_HEAP_RT_CACHE;
+
+	if (zend_ast_process) {
+		zend_ast_process(CG(ast));
+	}
+
+	zend_file_context_begin(&original_file_context);
+	zend_oparray_context_begin(&original_oparray_context, op_array);
+	zend_compile_top_stmt(CG(ast));
+	CG(zend_lineno) = last_lineno;
+	zend_emit_final_return(type == ZEND_USER_FUNCTION);
+	op_array->line_start = 1;
+	op_array->line_end = last_lineno;
+	pass_two(op_array);
+	zend_oparray_context_end(&original_oparray_context);
+	zend_file_context_end(&original_file_context);
+
+	CG(active_op_array) = original_active_op_array;
+
+	return op_array;
+}
+
 static zend_op_array *zend_compile(zend_function_type type)
 {
 	zend_op_array *op_array = NULL;
@@ -604,34 +641,7 @@ static zend_op_array *zend_compile(zend_function_type type)
 	CG(ast_arena) = zend_arena_create(1024 * 32);

 	if (!zendparse()) {
-		uint32_t last_lineno = CG(zend_lineno);
-		zend_file_context original_file_context;
-		zend_oparray_context original_oparray_context;
-		zend_op_array *original_active_op_array = CG(active_op_array);
-
-		op_array = emalloc(sizeof(zend_op_array));
-		init_op_array(op_array, type, INITIAL_OP_ARRAY_SIZE);
-		CG(active_op_array) = op_array;
-
-		/* Use heap to not waste arena memory */
-		op_array->fn_flags |= ZEND_ACC_HEAP_RT_CACHE;
-
-		if (zend_ast_process) {
-			zend_ast_process(CG(ast));
-		}
-
-		zend_file_context_begin(&original_file_context);
-		zend_oparray_context_begin(&original_oparray_context, op_array);
-		zend_compile_top_stmt(CG(ast));
-		CG(zend_lineno) = last_lineno;
-		zend_emit_final_return(type == ZEND_USER_FUNCTION);
-		op_array->line_start = 1;
-		op_array->line_end = last_lineno;
-		pass_two(op_array);
-		zend_oparray_context_end(&original_oparray_context);
-		zend_file_context_end(&original_file_context);
-
-		CG(active_op_array) = original_active_op_array;
+		op_array = zend_compile_ast_internal(type);
 	}

 	zend_ast_destroy(CG(ast));
@@ -674,6 +684,23 @@ ZEND_API zend_op_array *compile_file(zend_file_handle *file_handle, int type)
 	return op_array;
 }

+ZEND_API zend_op_array *zend_compile_ast(
+	zend_ast *ast, int type, zend_string *filename)
+{
+	zend_string *original_compiled_filename = CG(compiled_filename);
+	bool original_in_compilation = CG(in_compilation);
+	CG(in_compilation) = true;
+	CG(ast) = ast;
+
+	zend_set_compiled_filename(filename);
+	zend_op_array *op_array = zend_compile_ast_internal(type);
+
+	CG(in_compilation) = original_in_compilation;
+	zend_restore_compiled_filename(original_compiled_filename);
+
+	return op_array;
+}
+
 ZEND_API zend_ast *zend_compile_string_to_ast(
 		zend_string *code, zend_arena **ast_arena, zend_string *filename) {
 	zval code_zv;
diff --git a/Zend/zend_partial.c b/Zend/zend_partial.c
new file mode 100644
index 00000000000..4095daa1f1a
--- /dev/null
+++ b/Zend/zend_partial.c
@@ -0,0 +1,1163 @@
+/*
+   +----------------------------------------------------------------------+
+   | Zend Engine                                                          |
+   +----------------------------------------------------------------------+
+   | Copyright © Zend Technologies Ltd., a subsidiary company of          |
+   |     Perforce Software, Inc., and Contributors.                       |
+   +----------------------------------------------------------------------+
+   | This source file is subject to the Modified BSD License that is      |
+   | bundled with this package in the file LICENSE, and is available      |
+   | through the World Wide Web at <https://www.php.net/license/>.        |
+   |                                                                      |
+   | SPDX-License-Identifier: BSD-3-Clause                                |
+   +----------------------------------------------------------------------+
+   | Authors: Arnaud Le Blanc <arnaud.lb@gmail.com>                       |
+   +----------------------------------------------------------------------+
+*/
+
+/**
+ * Partial Function Application:
+ *
+ * A partial application is compiled to the usual sequence of function call
+ * opcodes (INIT_FCALL, SEND_VAR, etc), but the sequence ends with a
+ * CALLABLE_CONVERT_PARTIAL opcode instead of DO_FCALL, similarly to
+ * first class callables. Placeholders are compiled to SEND_PLACEHOLDER opcodes:
+ *
+ * $f = f($a, ?)
+ *
+ * 0001 INIT_FCALL f
+ * 0002 SEND_VAR CV($a)
+ * 0003 SEND_PLACEHOLDER
+ * 0004 CV($f) = CALLABLE_CONVERT_PARTIAL
+ *
+ * SEND_PLACEHOLDER sets the argument slot type to _IS_PLACEHOLDER.
+ *
+ * CALLABLE_CONVERT_PARTIAL uses the information available on the stack to
+ * create a Closure and return it, consuming the stack frame in the process
+ * like an internal function call.
+ *
+ * We create the Closure by generating the relevant AST and compiling it to an
+ * op_array. The op_array is cached in the Opcache SHM and inline caches.
+ *
+ * This file implements the Closure generation logic
+ * (see zend_partial_create(), zp_compile()).
+ */
+
+#include "zend.h"
+#include "zend_API.h"
+#include "zend_arena.h"
+#include "zend_ast.h"
+#include "zend_compile.h"
+#include "zend_closures.h"
+#include "zend_attributes.h"
+#include "zend_exceptions.h"
+#include "ext/opcache/ZendAccelerator.h"
+
+static zend_always_inline bool Z_IS_PLACEHOLDER_P(const zval *p) {
+	return Z_TYPE_P(p) == _IS_PLACEHOLDER;
+}
+
+static zend_always_inline bool zp_is_static_closure(const zend_function *function) {
+	return ((function->common.fn_flags & (ZEND_ACC_STATIC|ZEND_ACC_CLOSURE)) == (ZEND_ACC_STATIC|ZEND_ACC_CLOSURE));
+}
+
+static zend_always_inline bool zp_is_non_static_closure(const zend_function *function) {
+	return ((function->common.fn_flags & (ZEND_ACC_STATIC|ZEND_ACC_CLOSURE)) == ZEND_ACC_CLOSURE);
+}
+
+static zend_never_inline ZEND_COLD void zp_args_underflow(
+		const zend_function *function, uint32_t args, uint32_t expected)
+{
+	zend_string *symbol = get_function_or_method_name(function);
+	const char *limit = function->common.num_args <= function->common.required_num_args ?
+			"exactly" : "at least";
+
+	zend_argument_count_error(
+		"Partial application of %s() expects %s %d arguments, %d given",
+		ZSTR_VAL(symbol), limit, expected, args);
+
+	zend_string_release(symbol);
+}
+
+static zend_never_inline ZEND_COLD void zp_args_overflow(
+		const zend_function *function, uint32_t args, uint32_t expected)
+{
+	zend_string *symbol = get_function_or_method_name(function);
+
+	zend_argument_count_error(
+		"Partial application of %s() expects at most %d arguments, %d given",
+		ZSTR_VAL(symbol), expected, args);
+
+	zend_string_release(symbol);
+}
+
+static zend_result zp_args_check(const zend_function *function,
+		uint32_t argc, const zval *argv,
+		const zend_array *extra_named_args,
+		bool uses_variadic_placeholder) {
+
+	if (extra_named_args) {
+		ZEND_HASH_MAP_FOREACH_STR_KEY_VAL(extra_named_args, zend_string *key, zval *arg) {
+			if (UNEXPECTED(Z_IS_PLACEHOLDER_P(arg))) {
+				zend_throw_error(NULL,
+						"Cannot use named placeholder for unknown or variadic parameter $%s",
+						ZSTR_VAL(key));
+				return FAILURE;
+			}
+		} ZEND_HASH_FOREACH_END();
+	}
+
+	if (argc < function->common.required_num_args) {
+		if (uses_variadic_placeholder) {
+			/* Missing args will be turned into placeholders */
+			return SUCCESS;
+		}
+
+		zp_args_underflow(function, argc, function->common.required_num_args);
+		return FAILURE;
+	} else if (argc > function->common.num_args
+			&& !(function->common.fn_flags & ZEND_ACC_VARIADIC)) {
+		zp_args_overflow(function, argc, function->common.num_args);
+		return FAILURE;
+	}
+
+	return SUCCESS;
+}
+
+typedef struct zp_names {
+	zend_string *variadic_param;
+	zend_string *extra_named_params;
+	zend_string *inner_closure;
+	zend_string *params[1]; /* argc elements */
+} zp_names;
+
+static bool zp_name_exists(const zp_names *names, uint32_t argc, const zend_string *name)
+{
+	for (uint32_t i = 0; i < argc; i++) {
+		if (names->params[i] && zend_string_equals(names->params[i], name)) {
+			return true;
+		}
+	}
+	if (names->variadic_param && zend_string_equals(names->variadic_param, name)) {
+		return true;
+	}
+	if (names->extra_named_params && zend_string_equals(names->extra_named_params, name)) {
+		return true;
+	}
+	if (names->inner_closure && zend_string_equals(names->inner_closure, name)) {
+		return true;
+	}
+
+	return false;
+}
+
+static void zp_names_dtor(zp_names *names, uint32_t argc)
+{
+	for (uint32_t i = 0; i < argc; i++) {
+		if (names->params[i]) {
+			zend_string_release(names->params[i]);
+		}
+	}
+	if (names->variadic_param) {
+		zend_string_release(names->variadic_param);
+	}
+	if (names->extra_named_params) {
+		zend_string_release(names->extra_named_params);
+	}
+	if (names->inner_closure) {
+		zend_string_release(names->inner_closure);
+	}
+}
+
+static zend_string *zp_get_func_param_name(const zend_function *function, uint32_t arg_offset)
+{
+	return zend_string_copy(function->common.arg_info[arg_offset].name);
+}
+
+/* Assign a name for every variable that will be used in the generated closure,
+ * 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)
+{
+	zp_names *names = zend_arena_calloc(&CG(ast_arena),
+			1, zend_safe_address_guarded(argc, sizeof(*names->params), offsetof(zp_names, params)));
+
+	/* Assign names for params. We never rename those. */
+	for (uint32_t offset = 0, num_args = MIN(argc, function->common.num_args);
+			offset < num_args; offset++) {
+		if (Z_IS_PLACEHOLDER_P(&argv[offset])) {
+			names->params[offset] = zp_get_func_param_name(function, offset);
+		}
+	}
+
+	/* Assign name for the variadic param. Never renamed. */
+	if (variadic_partial && (function->common.fn_flags & ZEND_ACC_VARIADIC)) {
+		names->variadic_param = zp_get_func_param_name(function, function->common.num_args);
+	}
+
+	/* Assign names for placeholders that bind to the variadic param:
+	 *
+	 * function f($a, ...$args) {}
+	 * f(?, ?, ...); // The second placeholder binds into the variadic param.
+	 *
+	 * By default these are named $origNameN with N the offset from the
+	 * variadic param. In case of clash we increment N until a free name is
+	 * found. */
+	for (uint32_t offset = function->common.num_args; offset < argc; offset++) {
+		ZEND_ASSERT(function->common.fn_flags & ZEND_ACC_VARIADIC);
+		if (!Z_IS_PLACEHOLDER_P(&argv[offset])) {
+			continue;
+		}
+		zend_string *orig_name = zp_get_func_param_name(function, function->common.num_args);
+		zend_string *new_name;
+		for (uint32_t n = 0;; n++) {
+			new_name = zend_strpprintf_unchecked(0, "%S%" PRIu32, orig_name, n);
+			if (!zp_name_exists(names, argc, new_name)) {
+				break;
+			}
+			zend_string_release(new_name);
+		}
+		names->params[offset] = new_name;
+		zend_string_release(orig_name);
+	}
+
+	/* 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])) {
+			continue;
+		}
+		uint32_t n = 2;
+		zend_string *orig_name = zp_get_func_param_name(function, MIN(offset, function->common.num_args));
+		zend_string *new_name = zend_string_copy(orig_name);
+		while (zp_name_exists(names, argc, new_name)) {
+			zend_string_release(new_name);
+			new_name = zend_strpprintf_unchecked(0, "%S%" PRIu32, orig_name, n);
+			n++;
+		}
+		names->params[offset] = new_name;
+		zend_string_release(orig_name);
+	}
+
+	/* Assign name for $extra_named_params */
+	if (extra_named_params) {
+		uint32_t n = 2;
+		zend_string *new_name = ZSTR_INIT_LITERAL("extra_named_params", 0);
+		while (zp_name_exists(names, argc, new_name)) {
+			zend_string_release(new_name);
+			new_name = zend_strpprintf(0, "%s%" PRIu32, "extra_named_params", n);
+			n++;
+		}
+		names->extra_named_params = new_name;
+	}
+
+	/* Assign name for $fn */
+	if (function->common.fn_flags & ZEND_ACC_CLOSURE) {
+		uint32_t n = 2;
+		zend_string *new_name = ZSTR_INIT_LITERAL("fn", 0);
+		while (zp_name_exists(names, argc, new_name)) {
+			zend_string_release(new_name);
+			new_name = zend_strpprintf(0, "%s%" PRIu32, "fn", n);
+			n++;
+		}
+		names->inner_closure = new_name;
+	}
+
+	return names;
+}
+
+ZEND_ATTRIBUTE_CONST static inline bool zp_is_power_of_two(uint32_t x)
+{
+	return (x > 0) && !(x & (x - 1));
+}
+
+ZEND_ATTRIBUTE_CONST static bool zp_is_simple_type(uint32_t type_mask)
+{
+	ZEND_ASSERT(!(type_mask & ~_ZEND_TYPE_MAY_BE_MASK));
+
+	return zp_is_power_of_two(type_mask)
+		|| type_mask == MAY_BE_BOOL
+		|| type_mask == MAY_BE_ANY;
+}
+
+static zend_ast *zp_simple_type_to_ast(uint32_t type)
+{
+	zend_string *name;
+
+	switch (type) {
+		case MAY_BE_NULL:
+			name = ZSTR_KNOWN(ZEND_STR_NULL_LOWERCASE);
+			break;
+		case MAY_BE_TRUE:
+			name = ZSTR_KNOWN(ZEND_STR_TRUE);
+			break;
+		case MAY_BE_FALSE:
+			name = ZSTR_KNOWN(ZEND_STR_FALSE);
+			break;
+		case MAY_BE_LONG:
+			name = ZSTR_KNOWN(ZEND_STR_INT);
+			break;
+		case MAY_BE_DOUBLE:
+			name = ZSTR_KNOWN(ZEND_STR_FLOAT);
+			break;
+		case MAY_BE_STRING:
+			name = ZSTR_KNOWN(ZEND_STR_STRING);
+			break;
+		case MAY_BE_BOOL:
+			name = ZSTR_KNOWN(ZEND_STR_BOOL);
+			break;
+		case MAY_BE_VOID:
+			name = ZSTR_KNOWN(ZEND_STR_VOID);
+			break;
+		case MAY_BE_NEVER:
+			name = ZSTR_KNOWN(ZEND_STR_NEVER);
+			break;
+		case MAY_BE_OBJECT:
+			name = ZSTR_KNOWN(ZEND_STR_OBJECT);
+			break;
+		case MAY_BE_ANY:
+			name = ZSTR_KNOWN(ZEND_STR_MIXED);
+			break;
+		case MAY_BE_CALLABLE:
+			return zend_ast_create_ex(ZEND_AST_TYPE, IS_CALLABLE);
+		case MAY_BE_ARRAY:
+			return zend_ast_create_ex(ZEND_AST_TYPE, IS_ARRAY);
+		case MAY_BE_STATIC:
+			return zend_ast_create_ex(ZEND_AST_TYPE, IS_STATIC);
+		default:
+			ZEND_UNREACHABLE();
+	}
+
+	zend_ast *ast = zend_ast_create_zval_from_str(name);
+	ast->attr = ZEND_NAME_NOT_FQ;
+
+	return ast;
+}
+
+static zend_ast *zp_type_name_to_ast(zend_string *name)
+{
+	zend_ast *ast = zend_ast_create_zval_from_str(name);
+
+	if (zend_get_class_fetch_type(name) != ZEND_FETCH_CLASS_DEFAULT) {
+		ast->attr = ZEND_NAME_NOT_FQ;
+	} else {
+		ast->attr = ZEND_NAME_FQ;
+	}
+
+	return ast;
+}
+
+static zend_ast *zp_type_to_ast(const zend_type type)
+{
+	if (!ZEND_TYPE_IS_SET(type)) {
+		return NULL;
+	}
+
+	if (ZEND_TYPE_IS_UNION(type)
+			|| (ZEND_TYPE_IS_COMPLEX(type) && ZEND_TYPE_PURE_MASK(type))
+			|| (ZEND_TYPE_PURE_MASK(type) && !zp_is_simple_type(ZEND_TYPE_PURE_MASK(type)))) {
+		/* This is a union type */
+
+		zend_ast *type_ast = zend_ast_create_list(0, ZEND_AST_TYPE_UNION);
+
+		/* Add complex types if any */
+		if (ZEND_TYPE_HAS_LIST(type)) {
+			ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(type), const zend_type *type_ptr) {
+				type_ast = zend_ast_list_add(type_ast, zp_type_to_ast(*type_ptr));
+			} ZEND_TYPE_LIST_FOREACH_END();
+		} else if (ZEND_TYPE_HAS_NAME(type)) {
+			zend_ast *name_ast = zp_type_name_to_ast(
+					zend_string_copy(ZEND_TYPE_NAME(type)));
+			type_ast = zend_ast_list_add(type_ast, name_ast);
+		} else {
+			ZEND_ASSERT(!ZEND_TYPE_IS_COMPLEX(type));
+		}
+
+		/* Add simple types if any */
+		uint32_t type_mask = ZEND_TYPE_PURE_MASK(type);
+
+		/* IS_TRUE|IS_FALSE is represented as a single bool node */
+		if ((type_mask & MAY_BE_BOOL) == MAY_BE_BOOL) {
+			type_ast = zend_ast_list_add(type_ast, zp_simple_type_to_ast(MAY_BE_BOOL));
+			type_mask &= ~MAY_BE_BOOL;
+		}
+		for (uint32_t may_be_type = 1; may_be_type < _ZEND_TYPE_MAY_BE_MASK; may_be_type <<= 1) {
+			if (type_mask & may_be_type) {
+				type_ast = zend_ast_list_add(type_ast, zp_simple_type_to_ast(may_be_type));
+			}
+		}
+
+		return type_ast;
+	}
+
+	if (ZEND_TYPE_IS_INTERSECTION(type)) {
+		ZEND_ASSERT(!ZEND_TYPE_PURE_MASK(type));
+		zend_ast *type_ast = zend_ast_create_list(0, ZEND_AST_TYPE_INTERSECTION);
+		ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(type), const zend_type *type_ptr) {
+			type_ast = zend_ast_list_add(type_ast, zp_type_to_ast(*type_ptr));
+		} ZEND_TYPE_LIST_FOREACH_END();
+		return type_ast;
+	}
+
+	if (ZEND_TYPE_HAS_NAME(type)) {
+		ZEND_ASSERT(!ZEND_TYPE_PURE_MASK(type));
+		zend_ast *type_ast = zp_type_name_to_ast(
+				zend_string_copy(ZEND_TYPE_NAME(type)));
+		return type_ast;
+	}
+
+	ZEND_ASSERT(!ZEND_TYPE_IS_COMPLEX(type));
+
+	uint32_t type_mask = ZEND_TYPE_PURE_MASK(type);
+	ZEND_ASSERT(zp_is_simple_type(type_mask));
+
+	return zp_simple_type_to_ast(type_mask);
+}
+
+static zend_result zp_get_param_default_value(zval *result, zend_function *function, uint32_t arg_offset)
+{
+	ZEND_ASSERT(arg_offset < function->common.num_args);
+
+	if (function->type == ZEND_USER_FUNCTION) {
+		zend_op *opline = &function->op_array.opcodes[arg_offset];
+		if (EXPECTED(opline->opcode == ZEND_RECV_INIT)) {
+			ZVAL_COPY(result, RT_CONSTANT(opline, opline->op2));
+			return SUCCESS;
+		}
+		ZEND_ASSERT(opline->opcode == ZEND_RECV);
+	} else {
+		if (function->common.fn_flags & ZEND_ACC_USER_ARG_INFO) {
+			goto error;
+		}
+
+		const zend_arg_info *arg_info = &function->internal_function.arg_info[arg_offset];
+
+		if (zend_get_default_from_internal_arg_info(result, arg_info) == SUCCESS) {
+			return SUCCESS;
+		}
+	}
+
+error:
+	zend_argument_error_ex(zend_ce_argument_count_error, function, arg_offset + 1,
+			"must be passed explicitly, because the default value is not known");
+
+	return FAILURE;
+}
+
+static bool zp_arg_must_be_sent_by_ref(const zend_function *function, uint32_t arg_num)
+{
+	if (EXPECTED(arg_num <= MAX_ARG_FLAG_NUM)) {
+		if (QUICK_ARG_MUST_BE_SENT_BY_REF(function, arg_num)) {
+			return true;
+		}
+	} else if (ARG_MUST_BE_SENT_BY_REF(function, arg_num)) {
+		return true;
+	}
+	return false;
+}
+
+static zend_ast *zp_attribute_to_ast(zend_attribute *attribute)
+{
+	zend_ast *args_ast;
+	if (attribute->argc) {
+		args_ast = zend_ast_create_arg_list(0, ZEND_AST_ARG_LIST);
+		for (uint32_t i = 0; i < attribute->argc; i++) {
+			zend_ast *arg_ast = zend_ast_create_zval(&attribute->args[i].value);
+			if (attribute->args[i].name) {
+				arg_ast = zend_ast_create(ZEND_AST_NAMED_ARG,
+						zend_ast_create_zval_from_str(
+							zend_string_copy(attribute->args[i].name)),
+						arg_ast);
+			}
+			args_ast = zend_ast_list_add(args_ast, arg_ast);
+		}
+	} else {
+		args_ast = NULL;
+	}
+	return zend_ast_create(ZEND_AST_ATTRIBUTE,
+			zend_ast_create_zval_from_str(zend_string_copy(attribute->name)),
+			args_ast);
+}
+
+static zend_ast *zp_param_attributes_to_ast(zend_function *function,
+		uint32_t offset)
+{
+	zend_ast *attributes_ast = NULL;
+	if (!function->common.attributes) {
+		return NULL;
+	}
+
+	/* Inherit the SensitiveParameter attribute */
+	zend_attribute *attr = zend_get_parameter_attribute_str(
+			function->common.attributes,
+			"sensitiveparameter", strlen("sensitiveparameter"), offset);
+	if (attr) {
+		attributes_ast = zend_ast_create_list(1, ZEND_AST_ATTRIBUTE_GROUP,
+				zp_attribute_to_ast(attr));
+		attributes_ast = zend_ast_create_list(1, ZEND_AST_ATTRIBUTE_LIST,
+				attributes_ast);
+	}
+
+	return attributes_ast;
+}
+
+static zend_string *zp_pfa_name(const zend_op_array *declaring_op_array,
+		const zend_op *declaring_opline)
+{
+	/* We attempt to generate a name that hints at where the PFA was created,
+	 * similarly to Closures (GH-13550).
+	 * We do not attempt to make the name unique. */
+
+	zend_string *filename = declaring_op_array->filename;
+	uint32_t start_lineno = declaring_opline->lineno;
+
+	zend_string *class = zend_empty_string;
+	zend_string *separator = zend_empty_string;
+	zend_string *function = filename;
+	const char *parens = "";
+
+	if (declaring_op_array->function_name) {
+		function = declaring_op_array->function_name;
+		if (declaring_op_array->fn_flags & ZEND_ACC_CLOSURE) {
+			/* If the parent function is a closure, don't redundantly
+			 * add the classname and parentheses. */
+		} else {
+			parens = "()";
+
+			if (declaring_op_array->scope && declaring_op_array->scope->name) {
+				class = declaring_op_array->scope->name;
+				separator = ZSTR_KNOWN(ZEND_STR_PAAMAYIM_NEKUDOTAYIM);
+			}
+		}
+	}
+
+	zend_string *name = zend_strpprintf_unchecked(
+		0,
+		"{closure:pfa:%S%S%S%s:%" PRIu32 "}",
+		class,
+		separator,
+		function,
+		parens,
+		start_lineno
+	);
+
+	return name;
+}
+
+/* Generate the AST for calling the actual function */
+static zend_ast *zp_compile_forwarding_call(
+	zval *this_ptr, zend_function *function,
+	uint32_t argc, zval *argv, zend_array *extra_named_params,
+	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)
+{
+	bool is_assert = zend_string_equals(function->common.function_name,
+			ZSTR_KNOWN(ZEND_STR_ASSERT));
+
+	zend_ast *args_ast = zend_ast_create_list(0, ZEND_AST_ARG_LIST);
+	zend_ast *call_ast = NULL;
+
+	if (is_assert) {
+		/* We are going to call assert() dynamically (via call_user_func),
+		 * otherwise assert() would print the generated AST on failure, which is
+		 * irrelevant. */
+		args_ast = zend_ast_list_add(args_ast,
+				zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_ASSERT)));
+	}
+
+	/* Generate positional arguments */
+	for (uint32_t offset = 0; offset < argc; offset++) {
+		if (Z_ISUNDEF(argv[offset])) {
+			/* Argument was not passed. Pass its default value. */
+			if (offset < function->common.required_num_args) {
+				/* Required param was not passed. This can happen due to named
+				 * args. Using the same exception CE and message as
+				 * zend_handle_undef_args(). */
+				zend_argument_error_ex(zend_ce_argument_count_error, function, offset + 1, "not passed");
+				goto error;
+			}
+			zval default_value;
+			if (zp_get_param_default_value(&default_value, function, offset) == FAILURE) {
+				ZEND_ASSERT(EG(exception));
+				goto error;
+			}
+			zend_ast *default_value_ast;
+			if (Z_TYPE(default_value) == IS_CONSTANT_AST) {
+				/* Must dup AST because we are going to destroy it */
+				default_value_ast = zend_ast_dup(Z_ASTVAL(default_value));
+			} else {
+				default_value_ast = zend_ast_create_zval(&default_value);
+			}
+			args_ast = zend_ast_list_add(args_ast, default_value_ast);
+		} 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]))));
+		}
+	}
+	/* Use unpacking to pass extra named params */
+	if (extra_named_params) {
+		args_ast = zend_ast_list_add(args_ast, zend_ast_create(ZEND_AST_UNPACK,
+					zend_ast_create(ZEND_AST_VAR,
+						zend_ast_create_zval_from_str(zend_string_copy(var_names->extra_named_params)))));
+	}
+	if (uses_variadic_placeholder) {
+		if (function->common.fn_flags & ZEND_ACC_VARIADIC) {
+			/* Pass variadic params */
+			args_ast = zend_ast_list_add(args_ast, zend_ast_create(ZEND_AST_UNPACK,
+						zend_ast_create(ZEND_AST_VAR,
+							zend_ast_create_zval_from_str(zend_string_copy(var_names->variadic_param)))));
+		} else if (forward_superfluous_args) {
+			/* When a '...' placeholder is used, and the underlying function is
+			 * not variadic, superfluous arguments are forwarded.
+			 * Add a ...array_slice(func_get_args(), n) argument, which should
+			 * be compiled as ZEND_AST_UNPACK + ZEND_FUNC_GET_ARGS. */
+
+			zend_ast *func_get_args_name_ast = zend_ast_create_zval_from_str(
+					zend_string_copy(ZSTR_KNOWN(ZEND_STR_FUNC_GET_ARGS)));
+			func_get_args_name_ast->attr = ZEND_NAME_FQ;
+
+			zend_ast *array_slice_name_ast = zend_ast_create_zval_from_str(
+					zend_string_copy(ZSTR_KNOWN(ZEND_STR_ARRAY_SLICE)));
+			array_slice_name_ast->attr = ZEND_NAME_FQ;
+
+			args_ast = zend_ast_list_add(args_ast,
+				zend_ast_create(ZEND_AST_UNPACK,
+					zend_ast_create(ZEND_AST_CALL,
+							array_slice_name_ast,
+							zend_ast_create_list(2, ZEND_AST_ARG_LIST,
+								zend_ast_create(ZEND_AST_CALL,
+									func_get_args_name_ast,
+									zend_ast_create_list(0, ZEND_AST_ARG_LIST)),
+								zend_ast_create_zval_from_long(num_args)))));
+		}
+	}
+
+	if (is_assert) {
+		zend_ast *func_name_ast = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_CALL_USER_FUNC));
+		func_name_ast->attr = ZEND_NAME_FQ;
+		call_ast = zend_ast_create(ZEND_AST_CALL, func_name_ast, args_ast);
+	} else if (function->common.fn_flags & ZEND_ACC_CLOSURE) {
+		zend_ast *fn_ast = zend_ast_create(ZEND_AST_VAR,
+					zend_ast_create_zval_from_str(zend_string_copy(var_names->inner_closure)));
+		call_ast = zend_ast_create(ZEND_AST_CALL, fn_ast, args_ast);
+	} else if (Z_TYPE_P(this_ptr) == IS_OBJECT) {
+		zend_ast *this_ast = zend_ast_create(ZEND_AST_VAR,
+					zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_THIS)));
+		zend_ast *method_name_ast = zend_ast_create_zval_from_str(
+				zend_string_copy(function->common.function_name));
+		call_ast = zend_ast_create(ZEND_AST_METHOD_CALL, this_ast,
+				method_name_ast, args_ast);
+	} else if (called_scope) {
+		zend_ast *class_name_ast = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_STATIC));
+		class_name_ast->attr = ZEND_NAME_NOT_FQ;
+		zend_ast *method_name_ast = zend_ast_create_zval_from_str(
+				zend_string_copy(function->common.function_name));
+		call_ast = zend_ast_create(ZEND_AST_STATIC_CALL, class_name_ast,
+				method_name_ast, args_ast);
+	} else {
+		zend_ast *func_name_ast = zend_ast_create_zval_from_str(zend_string_copy(function->common.function_name));
+		func_name_ast->attr = ZEND_NAME_FQ;
+		call_ast = zend_ast_create(ZEND_AST_CALL, func_name_ast, args_ast);
+	}
+
+	/* Void functions can not 'return $expr' */
+	if (ZEND_TYPE_FULL_MASK(return_type) & MAY_BE_VOID) {
+		stmts_ast = zend_ast_list_add(stmts_ast, call_ast);
+	} else {
+		zend_ast *return_ast = zend_ast_create(ZEND_AST_RETURN, call_ast);
+		stmts_ast = zend_ast_list_add(stmts_ast, return_ast);
+	}
+
+	return stmts_ast;
+
+error:
+	zend_ast_destroy(args_ast);
+	zend_ast_destroy(call_ast);
+	return NULL;
+}
+
+static uint32_t zp_compute_num_required(const zend_function *function,
+		uint32_t orig_offset, uint32_t new_offset, uint32_t num_required) {
+	if (orig_offset < function->common.num_args) {
+		if (orig_offset < function->common.required_num_args) {
+			num_required = MAX(num_required, new_offset + 1);
+		}
+	} else {
+		ZEND_ASSERT(function->common.fn_flags & ZEND_ACC_VARIADIC);
+		/* Placeholders that run into the variadic portion become
+		 * required and make all params before them required */
+		ZEND_ASSERT(orig_offset >= num_required);
+		num_required = new_offset + 1;
+	}
+
+	return num_required;
+}
+
+/* Compile PFA to an op_array */
+static zend_op_array *zp_compile(zval *this_ptr, zend_function *function,
+		uint32_t argc, zval *argv, zend_array *extra_named_params,
+		const zend_array *named_positions,
+		const zend_op_array *declaring_op_array,
+		const zend_op *declaring_opline, void **cache_slot,
+		bool uses_variadic_placeholder) {
+
+	zend_op_array *op_array = NULL;
+
+	if (UNEXPECTED(function->common.fn_flags2 & ZEND_ACC2_FORBID_DYN_CALLS)) {
+		const char *format = "Cannot call %S() dynamically";
+		zend_throw_error(NULL, format, function->common.function_name);
+		return NULL;
+	}
+
+	if (UNEXPECTED(zp_args_check(function, argc, argv, extra_named_params, uses_variadic_placeholder) != SUCCESS)) {
+		ZEND_ASSERT(EG(exception));
+		return NULL;
+	}
+
+	zend_class_entry *called_scope;
+	if (Z_TYPE_P(this_ptr) == IS_OBJECT) {
+		called_scope = Z_OBJCE_P(this_ptr);
+	} else {
+		called_scope = Z_CE_P(this_ptr);
+	}
+
+	/* CG(ast_arena) is usually NULL, so we can't just make a snapshot */
+	zend_arena *orig_ast_arena = CG(ast_arena);
+	CG(ast_arena) = zend_arena_create(1024 * 4);
+
+	uint32_t orig_lineno = CG(zend_lineno);
+	CG(zend_lineno) = zend_get_executed_lineno();
+
+	uint32_t new_argc = argc;
+
+	if (uses_variadic_placeholder) {
+		/* A variadic placeholder generates an implicit positional placeholder
+		 * for any unspecified arg, so make room for those. */
+		new_argc = MAX(new_argc, function->common.num_args);
+	}
+
+	zval *tmp = zend_arena_alloc(&CG(ast_arena), zend_safe_address_guarded(new_argc, sizeof(*tmp), 0));
+	memcpy(tmp, argv, zend_safe_address_guarded(argc, sizeof(*tmp), 0));
+	argv = tmp;
+
+	/* Compute param positions and number of required args, add implicit
+	 * placeholders.
+	 *
+	 * Parameters are placed in the following order:
+	 * - Positional placeholders
+	 * - Then named placeholders in their syntax order
+	 * - Then implicit placeholders added by '...'
+	 */
+	uint32_t num_params = 0;
+	uint32_t num_required = 0;
+	uint32_t *arg_to_param_offset_map = zend_arena_alloc(&CG(ast_arena),
+			zend_safe_address_guarded(new_argc, sizeof(*arg_to_param_offset_map), 0));
+	{
+		uint32_t num_positional = 0;
+
+		/* First, we handle explicit placeholders */
+		for (uint32_t arg_offset = 0; arg_offset < argc; arg_offset++) {
+			if (!Z_IS_PLACEHOLDER_P(&argv[arg_offset])) {
+				continue;
+			}
+
+			num_params++;
+
+			zend_arg_info *arg_info = &function->common.arg_info[MIN(arg_offset, function->common.num_args)];
+			zval *named_pos = named_positions ? zend_hash_find(named_positions, arg_info->name) : NULL;
+			uint32_t param_offset;
+			if (named_pos) {
+				/* Placeholder is sent as named arg. 'num_positional' is fixed
+				 * at this point. */
+				param_offset = num_positional + Z_LVAL_P(named_pos);
+			} else {
+				/* Placeholder is sent as positional */
+				param_offset = num_positional++;
+			}
+
+			arg_to_param_offset_map[arg_offset] = param_offset;
+		}
+
+		num_required = num_params;
+
+		/* Handle implicit placeholders added by '...' */
+		if (uses_variadic_placeholder) {
+			for (uint32_t arg_offset = 0; arg_offset < new_argc; arg_offset++) {
+				if (arg_offset < argc && !Z_ISUNDEF(argv[arg_offset])) {
+					continue;
+				}
+
+				/* Unspecified parameters become placeholders */
+				Z_TYPE_INFO(argv[arg_offset]) = _IS_PLACEHOLDER;
+
+				num_params++;
+
+				uint32_t param_offset = num_params - 1;
+
+				arg_to_param_offset_map[arg_offset] = param_offset;
+
+				num_required = zp_compute_num_required(function,
+						arg_offset, param_offset, num_required);
+			}
+		}
+	}
+
+	argc = new_argc;
+
+	/* Assign variable names */
+
+	zp_names *var_names = zp_assign_names(argc, argv, function,
+			uses_variadic_placeholder, extra_named_params);
+
+	/* Generate AST */
+
+	zend_ast *lexical_vars_ast = zend_ast_create_list(0, ZEND_AST_CLOSURE_USES);
+	zend_ast *params_ast = zend_ast_create_list(0, ZEND_AST_ARG_LIST);
+	zend_ast *return_type_ast = NULL;
+	zend_ast *stmts_ast = zend_ast_create_list(0, ZEND_AST_STMT_LIST);
+	zend_ast *attributes_ast = NULL;
+
+	/* Generate AST for params and lexical vars */
+	{
+		/* The inner Closure, if any, is assumed to be the first lexical var by
+		 * do_closure_bind(). */
+		if (function->common.fn_flags & ZEND_ACC_CLOSURE) {
+			zend_ast *lexical_var_ast = zend_ast_create_zval_from_str(
+					zend_string_copy(var_names->inner_closure));
+			lexical_vars_ast = zend_ast_list_add(lexical_vars_ast, lexical_var_ast);
+		}
+
+		zend_ast **params = zend_arena_calloc(&CG(ast_arena), num_params, sizeof(*params));
+		for (uint32_t offset = 0; offset < argc; offset++) {
+			if (Z_IS_PLACEHOLDER_P(&argv[offset])) {
+				zend_arg_info *arg_info = &function->common.arg_info[MIN(offset, function->common.num_args)];
+
+				int param_flags = 0;
+				if (zp_arg_must_be_sent_by_ref(function, offset+1)) {
+					param_flags |= ZEND_PARAM_REF;
+				}
+
+				uint32_t param_offset = arg_to_param_offset_map[offset];
+				zend_ast *param_type_ast = zp_type_to_ast(arg_info->type);
+				zend_ast *default_value_ast = NULL;
+				if (param_offset >= num_required) {
+					zval default_value;
+					if (zp_get_param_default_value(&default_value, function, offset) == FAILURE) {
+						for (uint32_t i = 0; i < num_params; i++) {
+							zend_ast_destroy(params[i]);
+						}
+						goto error;
+					}
+					default_value_ast = zend_ast_create_zval(&default_value);
+				}
+
+				ZEND_ASSERT(offset < function->common.num_args || (function->common.fn_flags & ZEND_ACC_VARIADIC));
+
+				zend_ast *attributes_ast = zp_param_attributes_to_ast(function, MIN(offset, function->common.num_args));
+				params[param_offset] = zend_ast_create_ex(ZEND_AST_PARAM,
+						param_flags, param_type_ast,
+						zend_ast_create_zval_from_str(
+							zend_string_copy(var_names->params[offset])),
+						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;
+				}
+				lexical_vars_ast = zend_ast_list_add(
+						lexical_vars_ast, lexical_var_ast);
+			}
+		}
+
+		for (uint32_t i = 0; i < num_params; i++) {
+			params_ast = zend_ast_list_add(params_ast, params[i]);
+		}
+	}
+
+	if (extra_named_params) {
+		zend_ast *lexical_var_ast = zend_ast_create_zval_from_str(
+				zend_string_copy(var_names->extra_named_params));
+		lexical_vars_ast = zend_ast_list_add(lexical_vars_ast, lexical_var_ast);
+	}
+
+	/* If we have a variadic placeholder and the underlying function is
+	 * variadic, add a variadic param. */
+	if (uses_variadic_placeholder
+			&& (function->common.fn_flags & ZEND_ACC_VARIADIC)) {
+		zend_arg_info *arg_info = &function->common.arg_info[function->common.num_args];
+		int param_flags = ZEND_PARAM_VARIADIC;
+		if (zp_arg_must_be_sent_by_ref(function, function->common.num_args+1)) {
+			param_flags |= ZEND_PARAM_REF;
+		}
+		zend_ast *param_type_ast = zp_type_to_ast(arg_info->type);
+		zend_ast *attributes_ast = zp_param_attributes_to_ast(function, function->common.num_args);
+		params_ast = zend_ast_list_add(params_ast, zend_ast_create_ex(ZEND_AST_PARAM,
+				param_flags, param_type_ast,
+				zend_ast_create_zval_from_str(
+					zend_string_copy(var_names->variadic_param)),
+				NULL, attributes_ast, NULL, NULL));
+	}
+
+	zend_type return_type = {0};
+	if (function->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE) {
+		return_type = (function->common.arg_info-1)->type;
+		return_type_ast = zp_type_to_ast(return_type);
+	}
+
+	/**
+	 * Generate function body.
+	 *
+	 * If we may need to forward superflous arguments, do that conditionally, as
+	 * it's faster:
+	 *
+	 * if (func_num_args() <= n) {
+	 *    // normal call
+	 * } else {
+	 *    // call with superflous arg forwarding
+	 * }
+	 *
+	 * The func_num_args() call should be compiled to a single FUNC_NUM_ARGS op.
+	 */
+
+	if (uses_variadic_placeholder && !(function->common.fn_flags & ZEND_ACC_VARIADIC)) {
+		zend_ast *no_forwarding_ast = zend_ast_create_list(0, ZEND_AST_STMT_LIST);
+		zend_ast *forwarding_ast = zend_ast_create_list(0, ZEND_AST_STMT_LIST);
+
+		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);
+
+		if (!no_forwarding_ast) {
+			ZEND_ASSERT(EG(exception));
+			goto error;
+		}
+
+		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);
+
+		if (!forwarding_ast) {
+			ZEND_ASSERT(EG(exception));
+			zend_ast_destroy(no_forwarding_ast);
+			goto error;
+		}
+
+		zend_ast *func_num_args_name_ast = zend_ast_create_zval_from_str(
+				zend_string_copy(ZSTR_KNOWN(ZEND_STR_FUNC_NUM_ARGS)));
+		func_num_args_name_ast->attr = ZEND_NAME_FQ;
+
+		stmts_ast = zend_ast_list_add(stmts_ast,
+			zend_ast_create_list(2, ZEND_AST_IF,
+				zend_ast_create(ZEND_AST_IF_ELEM,
+					zend_ast_create_binary_op(ZEND_IS_SMALLER_OR_EQUAL,
+						zend_ast_create(ZEND_AST_CALL, func_num_args_name_ast,
+							zend_ast_create_list(0, ZEND_AST_ARG_LIST)),
+						zend_ast_create_zval_from_long(num_params)),
+					no_forwarding_ast),
+				zend_ast_create(ZEND_AST_IF_ELEM,
+					NULL,
+					forwarding_ast)));
+	} else {
+		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);
+
+		if (!stmts_ast) {
+			ZEND_ASSERT(EG(exception));
+			goto error;
+		}
+	}
+
+	/* Inherit the NoDiscard attribute */
+	if (function->common.attributes) {
+		zend_attribute *attr = zend_get_attribute_str(
+				function->common.attributes, "nodiscard", strlen("nodiscard"));
+		if (attr) {
+			attributes_ast = zend_ast_create_list(1, ZEND_AST_ATTRIBUTE_GROUP,
+					zp_attribute_to_ast(attr));
+			attributes_ast = zend_ast_create_list(1, ZEND_AST_ATTRIBUTE_LIST,
+					attributes_ast);
+		}
+	}
+
+	int closure_flags = function->common.fn_flags & ZEND_ACC_RETURN_REFERENCE;
+	zend_ast *closure_ast = zend_ast_create_decl(ZEND_AST_CLOSURE,
+			closure_flags, CG(zend_lineno), NULL,
+			NULL, params_ast, lexical_vars_ast, stmts_ast,
+			return_type_ast, attributes_ast);
+
+	if (Z_TYPE_P(this_ptr) != IS_OBJECT && !zp_is_non_static_closure(function)) {
+		((zend_ast_decl*)closure_ast)->flags |= ZEND_ACC_STATIC;
+	}
+
+#if ZEND_DEBUG
+	{
+		const char *tmp = getenv("DUMP_PFA_AST");
+		if (tmp && ZEND_ATOL(tmp)) {
+			zend_string *str = zend_ast_export("", closure_ast, "");
+			fprintf(stderr, "PFA AST: %s\n", ZSTR_VAL(str));
+			zend_string_release(str);
+		}
+	}
+#endif
+
+	zend_string *pfa_name = zp_pfa_name(declaring_op_array, declaring_opline);
+
+	op_array = zend_accel_compile_pfa(closure_ast, declaring_op_array,
+			declaring_opline, function, pfa_name);
+
+	zend_ast_destroy(closure_ast);
+
+clean:
+	zp_names_dtor(var_names, argc);
+	zend_arena_destroy(CG(ast_arena));
+	CG(ast_arena) = orig_ast_arena;
+	CG(zend_lineno) = orig_lineno;
+
+	return op_array;
+
+error:
+	zend_ast_destroy(lexical_vars_ast);
+	zend_ast_destroy(params_ast);
+	zend_ast_destroy(return_type_ast);
+	zend_ast_destroy(stmts_ast);
+	zend_ast_destroy(attributes_ast);
+	goto clean;
+}
+
+/* Get the op_array of a PFA from caches or compile it */
+static const zend_op_array *zp_get_op_array(zval *this_ptr, zend_function *function,
+		uint32_t argc, zval *argv, zend_array *extra_named_params,
+		const zend_array *named_positions,
+		const zend_op_array *declaring_op_array,
+		const zend_op *declaring_opline, void **cache_slot,
+		bool uses_variadic_placeholder) {
+
+	if (EXPECTED(function->type == ZEND_INTERNAL_FUNCTION
+					? cache_slot[0] == function
+					: cache_slot[0] == function->op_array.opcodes)) {
+		ZEND_ASSERT(!(function->common.fn_flags & ZEND_ACC_NEVER_CACHE));
+		return cache_slot[1];
+	}
+
+	const zend_op_array *op_array = zend_accel_pfa_cache_get(declaring_op_array,
+			declaring_opline, function);
+
+	if (UNEXPECTED(!op_array)) {
+		op_array = zp_compile(this_ptr, function, argc, argv,
+			extra_named_params, named_positions, declaring_op_array, declaring_opline,
+			cache_slot, uses_variadic_placeholder);
+	}
+
+	if (EXPECTED(op_array) && !(function->common.fn_flags & ZEND_ACC_NEVER_CACHE)) {
+		cache_slot[0] = function->type == ZEND_INTERNAL_FUNCTION
+			? (void*)function
+			: (void*)function->op_array.opcodes;
+		cache_slot[1] = (zend_op_array*)op_array;
+	}
+
+	return op_array;
+}
+
+/* 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_arg_info *arg_infos = function->common.arg_info;
+	uint32_t bind_offset = 0;
+
+	if (function->common.fn_flags & ZEND_ACC_CLOSURE) {
+		zval var;
+		ZVAL_OBJ(&var, ZEND_CLOSURE_OBJECT(function));
+		Z_ADDREF(var);
+		zend_closure_bind_var_ex(result, bind_offset, &var);
+		bind_offset += sizeof(Bucket);
+	}
+
+	for (uint32_t offset = 0; offset < argc; offset++) {
+		zval *var = &argv[offset];
+		if (Z_IS_PLACEHOLDER_P(var) || Z_ISUNDEF_P(var)) {
+			continue;
+		}
+		zend_arg_info *arg_info;
+		if (offset < function->common.num_args) {
+			arg_info = &arg_infos[offset];
+		} else if (function->common.fn_flags & ZEND_ACC_VARIADIC) {
+			arg_info = &arg_infos[function->common.num_args];
+		} else {
+			arg_info = NULL;
+		}
+		if (arg_info && ZEND_TYPE_IS_SET(arg_info->type)
+				&& UNEXPECTED(!zend_check_type_ex(&arg_info->type, var, 0, 0))) {
+			zend_verify_arg_error(function, arg_info, offset+1, var);
+			zval_ptr_dtor(result);
+			ZVAL_NULL(result);
+			return;
+		}
+		ZEND_ASSERT(zp_arg_must_be_sent_by_ref(function, offset+1) ? Z_ISREF_P(var) : !Z_ISREF_P(var));
+		zend_closure_bind_var_ex(result, bind_offset, var);
+		bind_offset += sizeof(Bucket);
+	}
+
+	if (extra_named_params) {
+		zval var;
+		ZVAL_ARR(&var, extra_named_params);
+		Z_ADDREF(var);
+		zend_closure_bind_var_ex(result, bind_offset, &var);
+	}
+}
+
+void zend_partial_create(zval *result, zval *this_ptr, zend_function *function,
+		uint32_t argc, zval *argv, zend_array *extra_named_params,
+		const zend_array *named_positions,
+		const zend_op_array *declaring_op_array,
+		const zend_op *declaring_opline, void **cache_slot,
+		bool uses_variadic_placeholder) {
+
+	const zend_op_array *op_array = zp_get_op_array(this_ptr, function, argc, argv,
+			extra_named_params, named_positions,
+			declaring_op_array, declaring_opline,
+			cache_slot, uses_variadic_placeholder);
+
+	if (UNEXPECTED(!op_array)) {
+		ZEND_ASSERT(EG(exception));
+		ZVAL_NULL(result);
+		return;
+	}
+
+	zend_class_entry *called_scope;
+	zval object;
+
+	if (Z_TYPE_P(this_ptr) == IS_OBJECT) {
+		called_scope = Z_OBJCE_P(this_ptr);
+	} else {
+		called_scope = Z_CE_P(this_ptr);
+	}
+
+	if (Z_TYPE_P(this_ptr) == IS_OBJECT && !zp_is_static_closure(function)) {
+		ZVAL_COPY_VALUE(&object, this_ptr);
+	} else {
+		ZVAL_UNDEF(&object);
+	}
+
+	zend_create_partial_closure(result, (zend_function*)op_array,
+			function->common.scope, called_scope, &object,
+			(function->common.fn_flags & ZEND_ACC_CLOSURE) != 0);
+
+	zp_bind(result, function, argc, argv, extra_named_params);
+}
+
+void zend_partial_op_array_dtor(zval *pDest)
+{
+	destroy_op_array(Z_PTR_P(pDest));
+}
diff --git a/Zend/zend_partial.h b/Zend/zend_partial.h
new file mode 100644
index 00000000000..3c47305ff54
--- /dev/null
+++ b/Zend/zend_partial.h
@@ -0,0 +1,35 @@
+/*
+   +----------------------------------------------------------------------+
+   | Zend Engine                                                          |
+   +----------------------------------------------------------------------+
+   | Copyright © Zend Technologies Ltd., a subsidiary company of          |
+   |     Perforce Software, Inc., and Contributors.                       |
+   +----------------------------------------------------------------------+
+   | This source file is subject to the Modified BSD License that is      |
+   | bundled with this package in the file LICENSE, and is available      |
+   | through the World Wide Web at <https://www.php.net/license/>.        |
+   |                                                                      |
+   | SPDX-License-Identifier: BSD-3-Clause                                |
+   +----------------------------------------------------------------------+
+   | Authors: Arnaud Le Blanc <arnaud.lb@gmail.com>                       |
+   +----------------------------------------------------------------------+
+*/
+#ifndef ZEND_PARTIAL_H
+#define ZEND_PARTIAL_H
+
+#include "zend_compile.h"
+
+BEGIN_EXTERN_C()
+
+void zend_partial_create(zval *result, zval *this_ptr, zend_function *function,
+		uint32_t argc, zval *argv, zend_array *extra_named_params,
+		const zend_array *named_positions,
+		const zend_op_array *declaring_op_array,
+		const zend_op *declaring_opline, void **cache_slot,
+		bool uses_variadic_placeholder);
+
+void zend_partial_op_array_dtor(zval *pDest);
+
+END_EXTERN_C()
+
+#endif
diff --git a/Zend/zend_string.h b/Zend/zend_string.h
index fdfff2bbc89..cb0502891ef 100644
--- a/Zend/zend_string.h
+++ b/Zend/zend_string.h
@@ -707,6 +707,11 @@ default: ZEND_UNREACHABLE();
 	_(ZEND_STR_AUTOGLOBAL_ENV,         "_ENV") \
 	_(ZEND_STR_AUTOGLOBAL_REQUEST,     "_REQUEST") \
 	_(ZEND_STR_COUNT,                  "count") \
+	_(ZEND_STR_FUNC_NUM_ARGS,          "func_num_args") \
+	_(ZEND_STR_FUNC_GET_ARGS,          "func_get_args") \
+	_(ZEND_STR_ASSERT,                 "assert") \
+	_(ZEND_STR_CALL_USER_FUNC,         "call_user_func") \
+	_(ZEND_STR_ARRAY_SLICE,            "array_slice") \
 	_(ZEND_STR_SENSITIVEPARAMETER,     "SensitiveParameter") \
 	_(ZEND_STR_CONST_EXPR_PLACEHOLDER, "[constant expression]") \
 	_(ZEND_STR_DEPRECATED_CAPITALIZED, "Deprecated") \
diff --git a/Zend/zend_types.h b/Zend/zend_types.h
index 56774e4e9d8..b30f53d7f91 100644
--- a/Zend/zend_types.h
+++ b/Zend/zend_types.h
@@ -943,6 +943,8 @@ static zend_always_inline uint32_t zend_gc_delref_ex(zend_refcounted_h *p, uint3

 #define IS_OBJ_LAZY_UNINITIALIZED   (1U<<31) /* Virtual proxy or uninitialized Ghost */
 #define IS_OBJ_LAZY_PROXY           (1U<<30) /* Virtual proxy (may be initialized) */
+#define OBJ_EXTRA_FLAG_PRIV_1       (1U<<29) /* Reserved for private use by the object itself */
+#define OBJ_EXTRA_FLAG_PRIV_2       (1U<<28) /* Reserved for private use by the object itself */

 #define OBJ_EXTRA_FLAGS(obj)		((obj)->extra_flags)

diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h
index 61f89435843..0e5efa6f826 100644
--- a/Zend/zend_vm_def.h
+++ b/Zend/zend_vm_def.h
@@ -5726,6 +5726,30 @@ ZEND_VM_HELPER(zend_verify_recv_arg_type_helper, ANY, ANY, zval *op_1)
 	ZEND_VM_NEXT_OPCODE();
 }

+ZEND_VM_HANDLER(213, ZEND_SEND_PLACEHOLDER, UNUSED, CONST|UNUSED|NUM)
+{
+	zval *arg;
+
+	if (OP2_TYPE == IS_CONST) {
+		/* Named placeholder */
+		USE_OPLINE
+		SAVE_OPLINE();
+		zend_string *arg_name = Z_STR_P(RT_CONSTANT(opline, opline->op2));
+		uint32_t arg_num;
+		arg = zend_handle_named_arg(&EX(call), arg_name, &arg_num, CACHE_ADDR(opline->result.num));
+		if (UNEXPECTED(!arg)) {
+			HANDLE_EXCEPTION();
+		}
+	} else {
+		/* Positional placeholder */
+		arg = ZEND_CALL_VAR(EX(call), opline->result.var);
+	}
+
+	Z_TYPE_INFO_P(arg) = _IS_PLACEHOLDER;
+
+	ZEND_VM_NEXT_OPCODE();
+}
+
 ZEND_VM_HOT_HANDLER(63, ZEND_RECV, NUM, UNUSED)
 {
 	USE_OPLINE
@@ -9842,6 +9866,45 @@ ZEND_VM_HANDLER(202, ZEND_CALLABLE_CONVERT, UNUSED, UNUSED, NUM|CACHE_SLOT)
 	ZEND_VM_NEXT_OPCODE();
 }

+ZEND_VM_HANDLER(212, ZEND_CALLABLE_CONVERT_PARTIAL, CACHE_SLOT, CONST|UNUSED, NUM)
+{
+	USE_OPLINE
+	SAVE_OPLINE();
+
+	zend_execute_data *call = EX(call);
+	void **cache_slot = CACHE_ADDR(opline->op1.num);
+	zval *named_positions = GET_OP2_ZVAL_PTR();
+
+	zend_partial_create(EX_VAR(opline->result.var),
+		&call->This, call->func,
+		ZEND_CALL_NUM_ARGS(call), ZEND_CALL_ARG(call, 1),
+		(ZEND_CALL_INFO(call) & ZEND_CALL_HAS_EXTRA_NAMED_PARAMS) ?
+			call->extra_named_params : NULL,
+		OP2_TYPE == IS_CONST ? Z_ARRVAL_P(named_positions) : NULL,
+		&EX(func)->op_array, opline, cache_slot,
+		opline->extended_value & ZEND_FCALL_USES_VARIADIC_PLACEHOLDER);
+
+	if (ZEND_CALL_INFO(call) & ZEND_CALL_HAS_EXTRA_NAMED_PARAMS) {
+		zend_array_release(call->extra_named_params);
+	}
+
+	if ((call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE)) {
+		zend_free_trampoline(call->func);
+	}
+
+	if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) {
+		OBJ_RELEASE(Z_OBJ(call->This));
+	} else if (ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE) {
+		OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func));
+	}
+
+	EX(call) = call->prev_execute_data;
+
+	zend_vm_stack_free_call_frame(call);
+
+	ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
+}
+
 ZEND_VM_HANDLER(208, ZEND_JMP_FRAMELESS, CONST, JMP_ADDR, NUM|CACHE_SLOT)
 {
 	USE_OPLINE
diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h
index bcf4df0c8e7..6b675837d3a 100644
Binary files a/Zend/zend_vm_execute.h and b/Zend/zend_vm_execute.h differ
diff --git a/Zend/zend_vm_execute.skl b/Zend/zend_vm_execute.skl
index beabe36688d..4e8f28270ba 100644
--- a/Zend/zend_vm_execute.skl
+++ b/Zend/zend_vm_execute.skl
@@ -1,4 +1,5 @@
 #include "Zend/zend_vm_opcodes.h"
+#include "Zend/zend_partial.h"

 {%DEFINES%}

diff --git a/Zend/zend_vm_handlers.h b/Zend/zend_vm_handlers.h
index 6f159519545..7ffe2c220a0 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 0ece3e6f0c6..9846e36037b 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 92b46e6628f..61147518d36 100644
Binary files a/Zend/zend_vm_opcodes.h and b/Zend/zend_vm_opcodes.h differ
diff --git a/configure.ac b/configure.ac
index 9014869fb94..b50339063fc 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1789,6 +1789,7 @@ PHP_ADD_SOURCES([Zend], m4_normalize([
     zend_observer.c
     zend_opcode.c
     zend_operators.c
+    zend_partial.c
     zend_property_hooks.c
     zend_ptr_stack.c
     zend_signal.c
diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c
index ac6d6787a6d..2c467dc1fc8 100644
--- a/ext/opcache/ZendAccelerator.c
+++ b/ext/opcache/ZendAccelerator.c
@@ -24,7 +24,9 @@
 #include "zend_compile.h"
 #include "ZendAccelerator.h"
 #include "zend_modules.h"
+#include "zend_operators.h"
 #include "zend_persist.h"
+#include "zend_portability.h"
 #include "zend_shared_alloc.h"
 #include "zend_accelerator_module.h"
 #include "zend_accelerator_blacklist.h"
@@ -48,6 +50,7 @@
 #include "zend_system_id.h"
 #include "ext/pcre/php_pcre.h"
 #include "ext/standard/basic_functions.h"
+#include "zend_vm_opcodes.h"

 #ifdef ZEND_WIN32
 # include "ext/standard/md5.h"
@@ -2005,6 +2008,202 @@ static bool check_persistent_script_access(const zend_persistent_script *persist
 	}
 }

+static const char hexchars[] = "0123456789abcdef";
+
+static char *zend_accel_uintptr_hex(char *dest, uintptr_t n)
+{
+	do {
+		*dest++ = hexchars[n & 0xf];
+		n >>= 4;
+	} while (n);
+
+	return dest;
+}
+
+/* Prevents collisions with real scripts, as we don't cache paths prefixed with
+ * a scheme, except file:// and phar://. */
+#define PFA_KEY_PREFIX "pfa://"
+
+static zend_string *zend_accel_pfa_key(const zend_op *declaring_opline,
+		const zend_function *called_function)
+{
+	const size_t max_key_len = strlen(PFA_KEY_PREFIX) + (sizeof(uintptr_t)*2) + strlen(":") + (sizeof(uintptr_t)*2);
+	zend_string *key = zend_string_alloc(max_key_len, 0);
+
+	char *dest = ZSTR_VAL(key);
+	dest = zend_mempcpy(dest, PFA_KEY_PREFIX, strlen(PFA_KEY_PREFIX));
+	dest = zend_accel_uintptr_hex(dest, (uintptr_t)declaring_opline);
+	*dest++ = ':';
+
+	const void *ptr;
+	if ((called_function->common.fn_flags & ZEND_ACC_CLOSURE)
+			&& called_function->type == ZEND_USER_FUNCTION) {
+		/* Can not use 'called_function' as part of the key, as it's an inner
+		 * pointer to a Closure, which may be freed. Use its opcodes instead.
+		 * zend_accel_compile_pfa() ensures to extend the lifetime of opcodes
+		 * in this case. */
+		ptr = called_function->op_array.opcodes;
+	} else {
+		ptr = called_function;
+	}
+	dest = zend_accel_uintptr_hex(dest, (uintptr_t)ptr);
+
+	*dest = '\0';
+	ZSTR_LEN(key) = dest - ZSTR_VAL(key);
+
+	return key;
+}
+
+const zend_op_array *zend_accel_pfa_cache_get(const zend_op_array *declaring_op_array,
+		const zend_op *declaring_opline, const zend_function *called_function)
+{
+	zend_string *key = zend_accel_pfa_key(declaring_opline, called_function);
+	zend_op_array *op_array = NULL;
+
+	/* A PFA is SHM-cacheable if the declaring_op_array and called_function are
+	 * cached. */
+	if (ZCG(accelerator_enabled)
+			&& !file_cache_only
+			&& !declaring_op_array->refcount
+			&& (called_function->type != ZEND_USER_FUNCTION || !called_function->op_array.refcount)) {
+		zend_persistent_script *persistent_script = zend_accel_hash_find(&ZCSG(hash), key);
+		if (persistent_script) {
+			op_array = persistent_script->script.main_op_array.dynamic_func_defs[0];
+			if (persistent_script->num_warnings) {
+				zend_emit_recorded_errors_ex(persistent_script->num_warnings,
+						persistent_script->warnings);
+			}
+		}
+	} else {
+		op_array = zend_hash_find_ptr(&EG(partial_function_application_cache), key);
+	}
+
+	zend_string_release(key);
+
+	return op_array;
+}
+
+zend_op_array *zend_accel_compile_pfa(zend_ast *ast,
+		const zend_op_array *declaring_op_array,
+		const zend_op *declaring_opline,
+		const zend_function *called_function,
+		zend_string *pfa_func_name)
+{
+	zend_begin_record_errors();
+	zend_op_array *op_array;
+
+	uint32_t orig_compiler_options = CG(compiler_options);
+
+	zend_try {
+		CG(compiler_options) |= ZEND_COMPILE_HANDLE_OP_ARRAY;
+		CG(compiler_options) |= ZEND_COMPILE_DELAYED_BINDING;
+		CG(compiler_options) |= ZEND_COMPILE_NO_CONSTANT_SUBSTITUTION;
+		CG(compiler_options) |= ZEND_COMPILE_IGNORE_OTHER_FILES;
+		CG(compiler_options) |= ZEND_COMPILE_IGNORE_OBSERVER;
+#ifdef ZEND_WIN32
+		/* On Windows, don't compile with internal classes. Shm may be attached from different
+		 * processes with internal classes living in different addresses. */
+		CG(compiler_options) |= ZEND_COMPILE_IGNORE_INTERNAL_CLASSES;
+#endif
+
+		op_array = zend_compile_ast(ast, ZEND_USER_FUNCTION, declaring_op_array->filename);
+
+		CG(compiler_options) = orig_compiler_options;
+	} zend_catch {
+		CG(compiler_options) = orig_compiler_options;
+		zend_emit_recorded_errors();
+		zend_free_recorded_errors();
+		zend_bailout();
+	} zend_end_try();
+
+	ZEND_ASSERT(op_array->num_dynamic_func_defs == 1);
+
+	zend_string_release(op_array->dynamic_func_defs[0]->function_name);
+	op_array->dynamic_func_defs[0]->function_name = pfa_func_name;
+
+	zend_string *key = zend_accel_pfa_key(declaring_opline, called_function);
+
+	/* Cache op_array only if the declaring op_array and the called function
+	 * are cached */
+	if (!ZCG(accelerator_enabled)
+			|| file_cache_only
+			|| declaring_op_array->refcount
+			|| (called_function->type == ZEND_USER_FUNCTION && called_function->op_array.refcount)
+			|| (ZCSG(restart_in_progress) && accel_restart_is_active())
+			|| (!ZCG(counted) && accel_activate_add() == FAILURE)) {
+		zend_op_array *script_op_array = op_array;
+		zend_op_array *op_array = script_op_array->dynamic_func_defs[0];
+		GC_ADDREF(op_array->function_name);
+		(*op_array->refcount)++;
+		destroy_op_array(script_op_array);
+		efree(script_op_array);
+
+		if ((called_function->common.fn_flags & ZEND_ACC_CLOSURE)
+				&& called_function->type == ZEND_USER_FUNCTION
+				&& called_function->op_array.refcount) {
+			/* Extend the lifetime of the called opcodes if
+			 * the called function is a closure.
+			 * See comment in zend_accel_pfa_key(). */
+			zend_op_array *copy = zend_arena_alloc(&CG(arena), sizeof(*copy));
+			memcpy(copy, called_function, sizeof(*copy));
+			zend_string_addref(copy->function_name);
+			(*copy->refcount)++;
+			/* Reference the copy in op_array->dynamic_func_defs so that it's
+			 * destroyed when op_array is destroyed. */
+			ZEND_ASSERT(!op_array->dynamic_func_defs && !op_array->num_dynamic_func_defs);
+			op_array->dynamic_func_defs = safe_emalloc(1, sizeof(*op_array->dynamic_func_defs), 0);
+			op_array->dynamic_func_defs[0] = copy;
+			op_array->num_dynamic_func_defs = 1;
+		}
+
+		zend_hash_add_new_ptr(&EG(partial_function_application_cache), key, op_array);
+		zend_string_release(key);
+
+		zend_emit_recorded_errors();
+		zend_free_recorded_errors();
+
+		return op_array;
+	}
+
+	zend_persistent_script *new_persistent_script = create_persistent_script();
+	new_persistent_script->script.main_op_array = *op_array;
+	efree_size(op_array, sizeof(*op_array));
+	new_persistent_script->script.filename = key;
+
+	if (ZCG(accel_directives).record_warnings) {
+		new_persistent_script->num_warnings = EG(errors).size;
+		new_persistent_script->warnings = EG(errors).errors;
+	}
+
+	HANDLE_BLOCK_INTERRUPTIONS();
+	SHM_UNPROTECT();
+
+	bool from_shared_memory;
+	/* See GH-17246: we disable GC so that user code cannot be executed during the optimizer run. */
+	bool orig_gc_state = gc_enable(false);
+	char *orig_file_cache = ZCG(accel_directives).file_cache;
+	/* Disable file_cache temporarily, as we can't guarantee consistency. */
+	ZCG(accel_directives).file_cache = NULL;
+	new_persistent_script = cache_script_in_shared_memory(new_persistent_script, NULL, &from_shared_memory);
+	ZCG(accel_directives).file_cache = orig_file_cache;
+	gc_enable(orig_gc_state);
+
+	SHM_PROTECT();
+	HANDLE_UNBLOCK_INTERRUPTIONS();
+
+	/* We may have switched to an existing persistent script that was persisted in
+	 * the meantime. Make sure to use its warnings if available. */
+	if (ZCG(accel_directives).record_warnings) {
+		EG(record_errors) = false;
+		zend_emit_recorded_errors_ex(new_persistent_script->num_warnings, new_persistent_script->warnings);
+	} else {
+		zend_emit_recorded_errors();
+	}
+	zend_free_recorded_errors();
+
+	return new_persistent_script->script.main_op_array.dynamic_func_defs[0];
+}
+
 /* zend_compile() replacement */
 zend_op_array *persistent_compile_file(zend_file_handle *file_handle, int type)
 {
diff --git a/ext/opcache/ZendAccelerator.h b/ext/opcache/ZendAccelerator.h
index 91642e288d3..ef7eea433c0 100644
--- a/ext/opcache/ZendAccelerator.h
+++ b/ext/opcache/ZendAccelerator.h
@@ -334,6 +334,16 @@ zend_string* ZEND_FASTCALL accel_new_interned_string(zend_string *str);

 uint32_t zend_accel_get_class_name_map_ptr(zend_string *type_name);

+const zend_op_array *zend_accel_pfa_cache_get(const zend_op_array *declaring_op_array,
+		const zend_op *declaring_opline,
+		const zend_function *called_function);
+
+zend_op_array *zend_accel_compile_pfa(zend_ast *ast,
+		const zend_op_array *declaring_op_array,
+		const zend_op *declaring_opline,
+		const zend_function *called_function,
+		zend_string *pfa_func_name);
+
 END_EXTERN_C()

 /* memory write protection */
diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c
index 6f550f0cf36..cb2791fb45a 100644
--- a/ext/opcache/jit/zend_jit.c
+++ b/ext/opcache/jit/zend_jit.c
@@ -301,6 +301,7 @@ static int zend_jit_needs_call_chain(zend_call_info *call_info, uint32_t b, cons
 					case ZEND_DO_FCALL_BY_NAME:
 					case ZEND_DO_FCALL:
 					case ZEND_CALLABLE_CONVERT:
+					case ZEND_CALLABLE_CONVERT_PARTIAL:
 						return 0;
 					case ZEND_SEND_VAL:
 					case ZEND_SEND_VAR:
@@ -386,6 +387,7 @@ static int zend_jit_needs_call_chain(zend_call_info *call_info, uint32_t b, cons
 				case ZEND_DO_FCALL_BY_NAME:
 				case ZEND_DO_FCALL:
 				case ZEND_CALLABLE_CONVERT:
+				case ZEND_CALLABLE_CONVERT_PARTIAL:
 					end = opline;
 					if (end - op_array->opcodes >= ssa->cfg.blocks[b].start + ssa->cfg.blocks[b].len) {
 						/* INIT_FCALL and DO_FCALL in different BasicBlocks */
@@ -865,6 +867,7 @@ static bool zend_jit_dec_call_level(uint8_t opcode)
 		case ZEND_DO_UCALL:
 		case ZEND_DO_FCALL_BY_NAME:
 		case ZEND_CALLABLE_CONVERT:
+		case ZEND_CALLABLE_CONVERT_PARTIAL:
 			return true;
 		default:
 			return false;
diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c
index b0b04456706..f3dbcc84c12 100644
--- a/ext/opcache/jit/zend_jit_ir.c
+++ b/ext/opcache/jit/zend_jit_ir.c
@@ -10112,7 +10112,7 @@ static int zend_jit_do_fcall(zend_jit_ctx *jit, const zend_op *opline, const zen
 	}

 	bool may_have_extra_named_params =
-		opline->extended_value == ZEND_FCALL_MAY_HAVE_EXTRA_NAMED_PARAMS &&
+		(opline->extended_value & ZEND_FCALL_MAY_HAVE_EXTRA_NAMED_PARAMS) &&
 		(!func || func->common.fn_flags & ZEND_ACC_VARIADIC);

 	if (!jit->reuse_ip) {
diff --git a/ext/opcache/jit/zend_jit_vm_helpers.c b/ext/opcache/jit/zend_jit_vm_helpers.c
index 85a81c1573b..b01c3aaac62 100644
--- a/ext/opcache/jit/zend_jit_vm_helpers.c
+++ b/ext/opcache/jit/zend_jit_vm_helpers.c
@@ -1057,7 +1057,8 @@ zend_jit_trace_stop ZEND_FASTCALL zend_jit_trace_execute(zend_execute_data  *ex,
 				TRACE_RECORD(ZEND_JIT_TRACE_DO_ICALL, 0, func);
 			}
 		} else if (opline->opcode == ZEND_INCLUDE_OR_EVAL
-				|| opline->opcode == ZEND_CALLABLE_CONVERT) {
+				|| opline->opcode == ZEND_CALLABLE_CONVERT
+				|| opline->opcode == ZEND_CALLABLE_CONVERT_PARTIAL) {
 			/* TODO: Support tracing JIT for ZEND_CALLABLE_CONVERT. */
 			stop = ZEND_JIT_TRACE_STOP_INTERPRETER;
 			break;
diff --git a/ext/standard/tests/array/array_map_foreach_optimization_006.phpt b/ext/standard/tests/array/array_map_foreach_optimization_006.phpt
new file mode 100644
index 00000000000..e21e3e0a9a8
--- /dev/null
+++ b/ext/standard/tests/array/array_map_foreach_optimization_006.phpt
@@ -0,0 +1,84 @@
+--TEST--
+array_map(): foreach optimization - PFA
+--EXTENSIONS--
+opcache
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.opt_debug_level=0x20000
+--FILE--
+<?php
+
+function plusn($x, $n) {
+    return $x + $n;
+}
+
+$array = range(1, 10);
+
+$foo = array_map(plusn(?, 2), $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 T2 = DO_ICALL
+0004 ASSIGN CV0($array) T2
+0005 TYPE_ASSERT 131079 string("array_map") CV0($array)
+0006 T2 = INIT_ARRAY 0 (packed) NEXT
+0007 V3 = FE_RESET_R CV0($array) 0015
+0008 T5 = FE_FETCH_R V3 T4 0015
+0009 INIT_FCALL 2 %d string("plusn")
+0010 SEND_VAL T4 1
+0011 SEND_VAL int(2) 2
+0012 T4 = DO_UCALL
+0013 T2 = ADD_ARRAY_ELEMENT T4 T5
+0014 JMP 0008
+0015 FE_FREE V3
+0016 ASSIGN CV1($foo) T2
+0017 INIT_FCALL 1 %d string("var_dump")
+0018 SEND_VAR CV1($foo) 1
+0019 DO_ICALL
+0020 RETURN int(1)
+LIVE RANGES:
+     2: 0007 - 0016 (tmp/var)
+     3: 0008 - 0015 (loop)
+     4: 0009 - 0010 (tmp/var)
+     5: 0009 - 0013 (tmp/var)
+
+plusn:
+     ; (lines=4, args=2, vars=2, tmps=1)
+     ; (after optimizer)
+     ; %s
+0000 CV0($x) = RECV 1
+0001 CV1($n) = RECV 2
+0002 T2 = ADD CV0($x) CV1($n)
+0003 RETURN T2
+array(10) {
+  [0]=>
+  int(3)
+  [1]=>
+  int(4)
+  [2]=>
+  int(5)
+  [3]=>
+  int(6)
+  [4]=>
+  int(7)
+  [5]=>
+  int(8)
+  [6]=>
+  int(9)
+  [7]=>
+  int(10)
+  [8]=>
+  int(11)
+  [9]=>
+  int(12)
+}
diff --git a/ext/standard/tests/array/array_map_foreach_optimization_007.phpt b/ext/standard/tests/array/array_map_foreach_optimization_007.phpt
new file mode 100644
index 00000000000..67654b9e764
--- /dev/null
+++ b/ext/standard/tests/array/array_map_foreach_optimization_007.phpt
@@ -0,0 +1,109 @@
+--TEST--
+array_map(): foreach optimization - unoptimizable PFA
+--EXTENSIONS--
+opcache
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.opt_debug_level=0x20000
+--FILE--
+<?php
+
+function plusn($x, $n) {
+    return $x + $n;
+}
+
+$array = range(1, 10);
+
+$foo = array_map(plusn(n: 2, ...), $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 T2 = DO_ICALL
+0004 ASSIGN CV0($array) T2
+0005 INIT_FCALL 2 %d string("array_map")
+0006 INIT_FCALL 0 %d string("plusn")
+0007 SEND_VAL int(2) string("n")
+0008 T2 = CALLABLE_CONVERT_PARTIAL 2
+0009 SEND_VAL T2 1
+0010 SEND_VAR CV0($array) 2
+0011 T2 = DO_ICALL
+0012 ASSIGN CV1($foo) T2
+0013 INIT_FCALL 1 %d string("var_dump")
+0014 SEND_VAR CV1($foo) 1
+0015 DO_ICALL
+0016 RETURN int(1)
+
+plusn:
+     ; (lines=4, args=2, vars=2, tmps=1)
+     ; (after optimizer)
+     ; %s
+0000 CV0($x) = RECV 1
+0001 CV1($n) = RECV 2
+0002 T2 = ADD CV0($x) CV1($n)
+0003 RETURN T2
+
+$_main:
+     ; (lines=4, args=0, vars=1, tmps=1)
+     ; (after optimizer)
+     ; %s:1-9
+0000 T1 = DECLARE_LAMBDA_FUNCTION 0
+0001 BIND_LEXICAL T1 CV0($n)
+0002 FREE T1
+0003 RETURN int(1)
+LIVE RANGES:
+     1: 0001 - 0002 (tmp/var)
+
+{closure:pfa:%s:9}:
+     ; (lines=18, args=1, vars=2, tmps=2)
+     ; (after optimizer)
+     ; %s:9-9
+0000 CV0($x) = RECV 1
+0001 BIND_STATIC CV1($n)
+0002 T3 = FUNC_NUM_ARGS
+0003 T2 = IS_SMALLER_OR_EQUAL T3 int(1)
+0004 JMPZ T2 0010
+0005 INIT_FCALL 2 %d string("plusn")
+0006 SEND_VAR CV0($x) 1
+0007 SEND_VAR CV1($n) 2
+0008 T2 = DO_UCALL
+0009 RETURN T2
+0010 INIT_FCALL 2 %d string("plusn")
+0011 SEND_VAR CV0($x) 1
+0012 SEND_VAR CV1($n) 2
+0013 T2 = FUNC_GET_ARGS int(1)
+0014 SEND_UNPACK T2
+0015 CHECK_UNDEF_ARGS
+0016 T2 = DO_UCALL
+0017 RETURN T2
+array(10) {
+  [0]=>
+  int(3)
+  [1]=>
+  int(4)
+  [2]=>
+  int(5)
+  [3]=>
+  int(6)
+  [4]=>
+  int(7)
+  [5]=>
+  int(8)
+  [6]=>
+  int(9)
+  [7]=>
+  int(10)
+  [8]=>
+  int(11)
+  [9]=>
+  int(12)
+}
diff --git a/win32/build/config.w32 b/win32/build/config.w32
index a0f26033306..8ec6b31a11a 100644
--- a/win32/build/config.w32
+++ b/win32/build/config.w32
@@ -241,7 +241,7 @@ ADD_SOURCES("Zend", "zend_language_parser.c zend_language_scanner.c \
 	zend_float.c zend_string.c zend_generators.c zend_virtual_cwd.c zend_ast.c \
 	zend_inheritance.c zend_smart_str.c zend_cpuinfo.c zend_observer.c zend_system_id.c \
 	zend_enum.c zend_fibers.c zend_atomic.c zend_hrtime.c zend_frameless_function.c zend_property_hooks.c \
-	zend_lazy_objects.c zend_autoload.c");
+	zend_lazy_objects.c zend_autoload.c zend_partial.c");
 ADD_SOURCES("Zend\\Optimizer", "zend_optimizer.c pass1.c pass3.c optimize_func_calls.c block_pass.c optimize_temp_vars_5.c nop_removal.c compact_literals.c zend_cfg.c zend_dfg.c dfa_pass.c zend_ssa.c zend_inference.c zend_func_info.c zend_call_graph.c zend_dump.c escape_analysis.c compact_vars.c dce.c sccp.c scdf.c");

 var PHP_ASSEMBLER = PATH_PROG({