[PHP-DEV] [RFC] Partial Function Application v2

On Tue, Jul 22, 2025, at 3:18 PM, Tim Düsterhus wrote:

Hi

Am 2025-07-22 22:02, schrieb Larry Garfield:

It seems the discussion has quieted down and wasn't particularly
contentious to begin with (whew), so we're just about ready for a vote.

Would it not be appropriate to answer unanswered questions in the
discussion (i.e. mine from 12 days ago) and making the previously
announced changes (i.e. the ones that Arnaud announced 20 days ago) to
the RFC text before claiming that the discussion has quieted down and
that the RFC is ready for a vote?

Best regards
Tim Düsterhus

Hm, I thought Arnaud had already addressed those, sorry. I'll have to try to replicate the debug output bits sometime tonight on the current branch to include them in the RFC text. What other changes there are unaccounted for? I thought we'd kept up on the behavioral ones.

--Larry Garfield

Hi

Am 2025-07-23 15:57, schrieb Larry Garfield:

What other changes there are unaccounted for? I thought we'd kept up on the behavioral ones.

The support for attributes (particularly #[\SensitiveParameter]) is not mentioned in the RFC either. And as I mentioned yesterday, the observer support that I specifically asked about is missing / broken.

In my email from July, 10th I also had some follow-up questions / suggestions regarding the naming and the stack trace behavior, because I believe the semantics that Arnaud described are confusing.

Best regards
Tim Düsterhus

Hi

I did not yet have the time to check the changes and your reply in detail, but I already wanted to follow-up on some of the points.

Am 2025-07-23 15:55, schrieb Larry Garfield:

$c = fn(string $s, Point $p, int $m = 0) => stuff(1, $s, 3.14, $m);

I don't think this is accurate? `$p` is unused.

The choice of parameters names makes it hard to understand the examples,
particularly when named parameters are used to provide arguments out of
order. Including the “position” in the name of the parameter would make
it easier to follow the example, since it is not necessary to look up
the signature of `stuff()` all the time. (it becomes manageable if you
read it as a sentence "is FPM")

LOL. That was completely unintentional. :slight_smile:

However, I have gone through and added numbers to the variable names to clarify their original ordering.

I believe you missed some. I'm still seeing a bare `$i` and also a `$4p` typo. Please double-check, possibly there are also other mistakes, I didn't check in detail.

So the same is true of a PFA:

I don't think that necessarily follows, because for PFAs there are two parameter lists that are relevant:

1. The parameter list of the resulting Closure.
2. The parameter list of the original function.

foo(a: ?, b: 2, c: ?);
foo(b: 2, a: ?, c: ?);
foo(c: ?, b: 2, a: ?);
foo(?, 2, ?);

All of those produce the same result.

I don't think they should. Specifically the (1) and (3) should not. My expectation is that:

     $f = foo(a: ?, b: 2, c: ?);
     $f(1, 3); // calls foo(1, 2, 3);

and

     $f = foo(c: ?, b: 2, a: ?);
     $f(1, 3); // calls foo(3, 2, 1);

The order of the question marks should match the order of the parameters in the resulting Closure, which is not necessarily the order of the order parameters of the original function.

Best regards
Tim Düsterhus

Hi

Am 2025-07-24 12:03, schrieb Tim Düsterhus:

I don't think they should. Specifically the (1) and (3) should not. My expectation is that:

    $f = foo(a: ?, b: 2, c: ?);
    $f(1, 3); // calls foo(1, 2, 3);

and

    $f = foo(c: ?, b: 2, a: ?);
    $f(1, 3); // calls foo(3, 2, 1);

The order of the question marks should match the order of the parameters in the resulting Closure, which is not necessarily the order of the order parameters of the original function.

To add to that: By respecting the order of question marks as written, it would also make PFA easier to understand and also more powerful. After sending the email earlier, I came across this doctrine/collections PR, while reviewing some dependency upgrades: Leverage new array functions by derrabus · Pull Request #424 · doctrine/collections · GitHub

When migrating the `findFirst()` method from a custom foreach loop to `array_find()`, they couldn't pass along the given callable directly, since Doctrine opted for a different parameter order. Thus they needed an intermediate Closure to swap the arguments. If the argument names for the function are known (which they are not in the Doctrine example, as it's an arbitrary Closure that is given), this would allow to swap arguments as necessary:

     function cb($key, $val) { … }

     array_find($array, cb(val: ?, key: ?)); // array_find expects $value, $key.

Best regards
Tim Düsterhus

On Thu, Jul 24, 2025, at 4:54 AM, Tim Düsterhus wrote:

Hi

Am 2025-07-23 15:57, schrieb Larry Garfield:

What other changes there are unaccounted for? I thought we'd kept up
on the behavioral ones.

The support for attributes (particularly #[\SensitiveParameter]) is not
mentioned in the RFC either. And as I mentioned yesterday, the observer
support that I specifically asked about is missing / broken.

In my email from July, 10th I also had some follow-up questions /
suggestions regarding the naming and the stack trace behavior, because I
believe the semantics that Arnaud described are confusing.

Best regards
Tim Düsterhus

For many of these I'm afraid I'll have to wait for Arnuad's return, as I cannot speak to them with confidence.

--Larry Garfield

On Thu, Jul 24, 2025, at 5:03 AM, Tim Düsterhus wrote:

Hi

I did not yet have the time to check the changes and your reply in
detail, but I already wanted to follow-up on some of the points.

Am 2025-07-23 15:55, schrieb Larry Garfield:

$c = fn(string $s, Point $p, int $m = 0) => stuff(1, $s, 3.14, $m);

I don't think this is accurate? `$p` is unused.

However, I have gone through and added numbers to the variable names to
clarify their original ordering.

I believe you missed some. I'm still seeing a bare `$i` and also a `$4p`
typo. Please double-check, possibly there are also other mistakes, I
didn't check in detail.

Thanks, I think I got them all now.

(Reply to the rest in the other reply.)

--Larry Garfield

On Thu, Jul 24, 2025, at 6:52 AM, Tim Düsterhus wrote:

Hi

Am 2025-07-24 12:03, schrieb Tim Düsterhus:

I don't think they should. Specifically the (1) and (3) should not. My
expectation is that:

    $f = foo(a: ?, b: 2, c: ?);
    $f(1, 3); // calls foo(1, 2, 3);

and

    $f = foo(c: ?, b: 2, a: ?);
    $f(1, 3); // calls foo(3, 2, 1);

The order of the question marks should match the order of the
parameters in the resulting Closure, which is not necessarily the order
of the order parameters of the original function.

That would be inconsistent with how named arguments work anywhere else. With a regular function call, you can list named args in any order you feel like and the engine will reorder them for you back to the original defined order. (My IDE keeps yelling at me to put them in order, but that's an IDE problem, not a language problem.) Having PFA behave differently feels needlessly confusing, especially since positional placeholders are supported, too.

$f = foo(?, a: ?, b: ?)

It's not obvious to me what should happen there. Does that implicitly mean $c is now the first argument? That's not at all apparent from the syntax.

Similarly, in a longer example:

foo ($a, $b, $c, $d, $e, $f);

$f = foo(?, 3, b: ?, e: ?, ...);

That already takes a moment of thought to know what's going on, despite being legal. (A value is provided for $c, but nothing else.) Having that also change the order to... I think it would be f($a, $b, $e, $d, $f) ? Just raises the effort to grok it even further.

The only value to breaking existing convention and reordering parameters I can see would be:

To add to that: By respecting the order of question marks as written, it
would also make PFA easier to understand and also more powerful. After
sending the email earlier, I came across this doctrine/collections PR,
while reviewing some dependency upgrades:
Leverage new array functions by derrabus · Pull Request #424 · doctrine/collections · GitHub

When migrating the `findFirst()` method from a custom foreach loop to
`array_find()`, they couldn't pass along the given callable directly,
since Doctrine opted for a different parameter order. Thus they needed
an intermediate Closure to swap the arguments. If the argument names for
the function are known (which they are not in the Doctrine example, as
it's an arbitrary Closure that is given), this would allow to swap
arguments as necessary:

     function cb($key, $val) { … }

     array_find($array, cb(val: ?, key: ?)); // array_find expects
$value, $key.

Which is valid, but on balance I think it's OK for that case to still be a manual short-closure in exchange for less confusing behavior of PFA. In practice, I expect most PFA uses to be creating unary function anyway, followed by thunks, so it won't greatly matter.

Does anyone else agree/disagree?

--Larry Garfield

Hi

On 7/24/25 16:04, Larry Garfield wrote:

I don't think they should. Specifically the (1) and (3) should not. My
expectation is that:

     $f = foo(a: ?, b: 2, c: ?);
     $f(1, 3); // calls foo(1, 2, 3);

and

     $f = foo(c: ?, b: 2, a: ?);
     $f(1, 3); // calls foo(3, 2, 1);

The order of the question marks should match the order of the
parameters in the resulting Closure, which is not necessarily the order
of the order parameters of the original function.

That would be inconsistent with how named arguments work anywhere else. With a regular function call, you can list named args in any order you feel like and the engine will reorder them for you back to the original defined order. (My IDE keeps yelling at me to put them in order, but that's an IDE problem, not a language problem.) Having PFA behave differently feels needlessly confusing, especially since positional placeholders are supported, too.

The point is that PFA defines not just a function *call*, but also a function *definition* at the same time. This is what I meant by “you are dealing with two parameter lists”. What you correctly describe is the behavior of named arguments in a function *call*. What we do not have yet is the behavior for a function *definition* and I'm arguing that the proposed behavior in the RFC is wrong.

Using the existing example of:

     function foo(int $a, int $b, int $c) {}

For the first case of:

     $f = foo(a: ?, b: 2, c: ?);

My mental model desugars that to:

     $f = fn ($a, $c) => foo(a: $a, b: 2, c: $c);

And for the second case:

     $f = foo(c: ?, b: 2, a: ?);

it desugars to:

     $f = fn ($c, $a) => foo(c: $c, b: 2, a: $a);

The order of the named parameters in the inner function call to `foo()` changed, but as you said, the engine will make sure to place them into the appropriate parameters, resulting in a successful call to `foo()`.

But it does not follow that the parameters of the PFA Closure also need to be reordered, they can (and should) be completely independent, because PFA allows to “call named parameters using positional arguments”. Basically when I'm reading the signature:

     $f = foo(c: ?, b: 2, a: ?);
     $f(1, 3);

I'm reading the question-marks from “left to right” in positional order and then when I call `$f` using positional arguments, I'm expecting the question-marks to be filled in in positional order.

------

I've also asked a friend of mine, who had only skimmed the RFC a while ago, without explaining anything:

     function foo($a, $b, $c) {
       var_dump($a, $b, $c);
     }

     $f = foo(c: ?, b: 2, a: ?);
     $f(1, 3);

What would your intuitive expectation what this would output? They answered "3 2 1" and after I clarified that the RFC specifies this as "1 2 3", their first reaction was "What?", followed by "there are two placeholders, they are filled in left-to-right".

$f = foo(?, a: ?, b: ?)

It's not obvious to me what should happen there. Does that implicitly mean $c is now the first argument? That's not at all apparent from the syntax.

I believe this should be a compiler error, because `$a` is specified twice.

At least it would be for a normal function call: Online PHP editor | output for oVMLC

Similarly, in a longer example:

foo ($a, $b, $c, $d, $e, $f);

$f = foo(?, 3, b: ?, e: ?, ...);

That already takes a moment of thought to know what's going on, despite being legal. (A value is provided for $c, but nothing else.) Having that also change the order to... I think it would be f($a, $b, $e, $d, $f) ? Just raises the effort to grok it even further.

Likewise `b` is specified twice.

Best regards
Tim Düsterhus

On Tue, Jul 22, 2025, at 3:02 PM, Larry Garfield wrote:

Hi folks. Just a quick update: We've made one small change to the RFC.
Specifically, in order to prevent accidentally calling optional
arguments from callback locations like array_map() or array_find(), a
partial created with foo(?) will ignore any additional arguments passed
to it, and will not pass those through to the underlying function. A
partial that uses foo(?, ...) will pass through whatever it gets.

This is mainly to avoid passing an array key from those functions to a
callback function that has an optional second parameter, which is not
intended to get a key string. In practice this is what most people
would expect would happen, but we're calling it out explicitly. (I'm
not even sure it's a behavior change from what we had before, in
practice.)

cf:
PHP: rfc:partial_function_application_v2

It seems the discussion has quieted down and wasn't particularly
contentious to begin with (whew), so we're just about ready for a vote.
However, Arnaud went on vacation and didn't remember to tell me when
he'd be back. :slight_smile: So I'm going to wait a few more days just in case he
has any last minute comments, but start the vote either when he returns
or Monday the 28th, whichever comes first. (That gets the vote
complete before the deadline for 8.5.)

--Larry Garfield

Hi folks. Arnaud and I have decided to hold off on the PFA vote for a while to give time to address late-arriving questions and finish off the implementation. That means it's now pushed to 8.6, and I've updated the RFC accordingly. We're not pausing though, so expect another update soon-ish. Meanwhile, if anyone else wants to weigh in on the trailing args question or repositioning parameters question, now is the time.

--Larry Garfield

On Sat, Jun 28, 2025, at 12:06 AM, Larry Garfield wrote:

Hi folks. Arnaud and I would like to present take-2 at Partial
Function Application.

PHP: rfc:partial_function_application_v2

It is largely similar to the previous PFA proposal from 2021, though
there are a number of changes. Most notably:

* The implementation is simpler, because FCC already did part of the
work. This RFC can build on it.
* Constructors are not supported.
* But optional arguments and named placeholders are supported.
* It includes pipe-based optimizations.

Note: We realize that this is a non-trivial RFC coming late in the
cycle. We are proposing it now because, well, it's ready now. If the
discussion goes smoothly, we're OK calling a vote on it for 8.5,
especially as it would complement pipes so well. If the discussion
runs longer, we're also OK with targeting 8.6 instead. We'll see how
that goes.

<floor opens for discussion, Larry falls through the trap door>

Hi folks. PFA is back. :slight_smile:

Since our last episode, Arnaud has greatly revised the implementation. Rather than emulating a closure and all the associated behavior, the new approach compiles PFAs into normal closures at runtime, leveraging opcache. The resulting closure is then "just a closure," and will behave like any other. That means, for instance, its behavior in debugging, reflection, etc. is all self-evident.

There were a few small behavior changes as a result, but not dramatically. Mainly it impacted variadic cases, where parameter names are now auto-generated when appropriate rather than simply being unnamed.

We have also changed the order of placeholders from "positional, variadic, named" to "positional, named, variadic." Meaning the `...` "and the rest" placeholder is always at the end of the call.

One outstanding question is whether to allow reordering of parameters in the PFA closure by using named arguments. With this implementation, Arnaud says it's possible to do if we decide to. I am still concerned that it would create too much complexity and confusion in practice. But we're willing to go with a broad consensus if it emerges.

--Larry Garfield

Hi

Am 2025-10-09 20:54, schrieb Larry Garfield:

One outstanding question is whether to allow reordering of parameters in the PFA closure by using named arguments. With this implementation, Arnaud says it's possible to do if we decide to. I am still concerned that it would create too much complexity and confusion in practice. But we're willing to go with a broad consensus if it emerges.

I still believe that this is *less* confusing, as I outlined before.

PHP: rfc:partial_function_application_v2

I've given it another read. Remarks:

Positional placeholders that map to the variadic portion of the underlying function will be named $args0, $args1, etc., and may be called by name if desired.

What will happen if the original function already has a parameter named $args0?

stuff($1i, $s2, $f3, $p4, $m5);

Typo: 1i.

Prefill all parameters, making a "delayed call" or "thunk"

The example desugaring is inconsistent with the previously explained semantics: Here an arbitrary number of arguments is accepted, but previously it says "will result in a Closure with no parameters".

// Placeholders may be named, too. Their order doesn't
// matter as long as they come after the ..., if any.

I believe this example is outdated, since the ... must come last now. (the 's' and 'i' parameters are also missing the "number").

$c = fn(int $i1, ?float $f3, Point $p1, Point $p2): string => things($i1, $f3, $p1, $p2);

This example desugaring seems to be incorrect with regard to the naming of the variadic parameters.

(four(c: ?, d: 4, b: ?, a: 1))(2, 3);

Just to spell this out explicitly: I believe this should print "1, 3, 2, 4".

$eMaker = fn(int y): E => E::make(1, $y);

`$` missing before `y`.

$c = stuff(?, ?, ?, ?, ?, ?);

The error message for this one appears to be a copy and paste error.

// throws Error(Named parameter $i overwrites previous place holder)

Is it actually "place holder" with a space?

$c = stuff(i:1, ?, ?, ?, ?);

Minor formatting nit: Missing space after colon.

$inter = fn(mixed ...$args): int => intval(..$args);

Typo: Missing dot in splat.

------

Questions:

- Are PFAs legal in constant expressions? e.g. does the following work: `const Foo = intval(?, 10);`?
- The RFC says that it compiles down to the equivalent Closure. Can you add an example of a stack trace for completeness? Ideally one with SensitiveParameter working. That will then showcase the closure naming, SensitiveParameters and that there are two stack frames for the call.

Best regards
Tim Düsterhus

On Thu, Oct 9, 2025, at 4:39 PM, Tim Düsterhus wrote:

I've given it another read. Remarks:

Positional placeholders that map to the variadic portion of the
underlying function will be named $args0, $args1, etc., and may be
called by name if desired.

What will happen if the original function already has a parameter named
$args0?

It will skip over existing names. I've updated the text accordingly.

Prefill all parameters, making a "delayed call" or "thunk"

The example desugaring is inconsistent with the previously explained
semantics: Here an arbitrary number of arguments is accepted, but
previously it says "will result in a Closure with no parameters".

Hm, I think that should probably read "no required parameters". It would only make a difference if the underlying function had an optional variadic AND you called the thunk with extra, extraneous args.

*Various typos fixed along the way*

------

Questions:

- Are PFAs legal in constant expressions? e.g. does the following work:
`const Foo = intval(?, 10);`?

At the moment no, though Arnaud says that should be doable, with some reasonable restrictions. (No variables, no variable callable name, etc.)

- The RFC says that it compiles down to the equivalent Closure. Can you
add an example of a stack trace for completeness? Ideally one with
SensitiveParameter working. That will then showcase the closure naming,
SensitiveParameters and that there are two stack frames for the call.

Best regards
Tim Düsterhus

I've asked Arnaud to generate one for the RFC.

--Larry Garfield

Hi

On 10/10/25 17:26, Larry Garfield wrote:

What will happen if the original function already has a parameter named
$args0?

It will skip over existing names. I've updated the text accordingly.

Okay. Looking at all the examples, I think it would be nice if it would not use the generic `arg` prefix, but use the original name as the prefix. For the

     function foo(int $a = 5, int $b = 1, string ...$c) { }

example, the 3rd and following parameters could be `$c_1`, `$c_2`, … (still skipping over duplicates).

Prefill all parameters, making a "delayed call" or "thunk"

The example desugaring is inconsistent with the previously explained
semantics: Here an arbitrary number of arguments is accepted, but
previously it says "will result in a Closure with no parameters".

Hm, I think that should probably read "no required parameters". It would only make a difference if the underlying function had an optional variadic AND you called the thunk with extra, extraneous args.

Okay. Now looking at the examples:

     $c = stuff(?, ?, f3: 3.5, p4: $point, ...);
     $c = fn(int $i1, string $s2, int $m5 = 0): string => stuff($i1, $s2, 3.5, $point, $m5);

and

     $c = stuff(1, 'hi', 3.4, $point, 5, ...);
     $c = fn(...$args): string => stuff(1, 'hi', 3.4, $point, 5, ...$args);

seem to be inconsistent with each other with regard to whether or not "superfluous" arguments are passed through.

This might (or might not?) be explained in the “func_get_args() and friends” section, but is unexplained at the point the example appears. Also within the func_get_args() section, there is no explicit `...$args` in the resulting signature, but instead the desugaring uses array_slice() + func_get_args().

- Are PFAs legal in constant expressions? e.g. does the following work:
`const Foo = intval(?, 10);`?

At the moment no, though Arnaud says that should be doable, with some reasonable restrictions. (No variables, no variable callable name, etc.)

Yes, restrictions go without saying. The support for FCC in const-expr also doesn't support variable names and Closures in const-expr may also not capture the scope.

I've asked Arnaud to generate one for the RFC.

Thank you, I'm seeing you added an example. I've a small request to hopefully make the example more useful: It currently uses only “placeholder” parameters. It would be useful to also add one pre-filled parameter to it. I would assume that this pre-filled parameter does not appear within the Closure frame, but appears with the frame for the actual function?

Relatedly to the stack trace example and also to the “Evaluation order” I'd be curious how pre-filled parameters are implemented technically. For pre-filled variables, the regular variable capturing logic would work, but it doesn't for function returns. Is it effectively creating a temporary variable under the hood? Meaning is:

     $partial = speak(?, getArg());

desugared into:

     $pfa_tmp_2 = getArg();
     $partial = fn (string $who) => speak($who, $pfa_tmp2);

?

- How does it interact with `compact()`, specifically:

     $partial = compact(someValue(), ?);

Would the `?` be able to capture a variable containing the return value of `someValue()`?

- How does it interact with `ReflectionFunctionAbstract::getClosureUsedVariables()`?
- Same question for other scope introspection functionality (e.g. Xdebug).

Best regards
Tim Düsterhus

Hi Tim,

On Sun, Oct 12, 2025 at 8:36 PM Tim Düsterhus <tim@bastelstu.be> wrote:

Relatedly to the stack trace example and also to the “Evaluation order”
I'd be curious how pre-filled parameters are implemented technically.
For pre-filled variables, the regular variable capturing logic would
work, but it doesn't for function returns. Is it effectively creating a
temporary variable under the hood? Meaning is:

     $partial = speak(?, getArg());

desugared into:

     $pfa_tmp_2 = getArg();
     $partial = fn (string $who) => speak($who, $pfa_tmp2);

?

Pre-filled parameters are passed to the Closure via used vars, but
there is no requirement for them to be in a CV slot so we don't need
to create a temporary variable. We can bind used vars to any zval
directly.

The code

    $partial = speak(?, getArg());

Is compiled to

    INIT_FCALL speak
    SEND_PLACEHOLDER
    INIT_FCALL getArg
    T1 = DO_FCALL
    SEND T1
    CALLABLE_CONVERT_PARTIAL

The CALLABLE_CONVERT_PARTIAL opcode generates the AST for

    function (string $who) use ($msg) {
       return \speak($who, $msg);
    };

and compiles it (this is cached in SHM). It then instantiates the
Closure, and binds $msg to ZEND_CALL_ARG(2).

A few other values are passed via used vars, such as the object if
it's an instance method call, or the underlying function if it's a
closure.

- How does it interact with
`ReflectionFunctionAbstract::getClosureUsedVariables()`?
- Same question for other scope introspection functionality (e.g. Xdebug).

getClosureUsedVariables() and other introspection functionality will
report any used var as usual. Used vars are named after the parameter
if there is no collision. But this should not be considered API as
this may change. For instance, I want to optimize-out used vars for
parameters that are pre-filled with a literal. It would be possible to
hide used vars of a PFA in Reflection.

Best Regards,
Arnaud

On Sun, Oct 12, 2025, at 1:35 PM, Tim Düsterhus wrote:

Hi

On 10/10/25 17:26, Larry Garfield wrote:

What will happen if the original function already has a parameter named
$args0?

It will skip over existing names. I've updated the text accordingly.

Okay. Looking at all the examples, I think it would be nice if it would
not use the generic `arg` prefix, but use the original name as the
prefix. For the

     function foo(int $a = 5, int $b = 1, string ...$c) { }

example, the 3rd and following parameters could be `$c_1`, `$c_2`, …
(still skipping over duplicates).

OK, there was a miscommunication between Arnaud and I. It is using the variable name, not "args" already. Just with no _ and 0-based. I've updated the RFC accordingly.

Okay. Now looking at the examples:

     $c = stuff(?, ?, f3: 3.5, p4: $point, ...);
     $c = fn(int $i1, string $s2, int $m5 = 0): string => stuff($i1,
$s2, 3.5, $point, $m5);

and

     $c = stuff(1, 'hi', 3.4, $point, 5, ...);
     $c = fn(...$args): string => stuff(1, 'hi', 3.4, $point, 5, ...$args);

seem to be inconsistent with each other with regard to whether or not
"superfluous" arguments are passed through.

This might (or might not?) be explained in the “func_get_args() and
friends” section, but is unexplained at the point the example appears.
Also within the func_get_args() section, there is no explicit `...$args`
in the resulting signature, but instead the desugaring uses
array_slice() + func_get_args().

We talked a bit more, and decided to tighten the rules further. I've updated the RFC accordingly. Essentially, the first example is correct, the second has been changed.

If the underlying function is variadic, and ... is used in the PFA, then it will accept an arbitrary number of arguments. In any other case, only explicitly-specified arguments will be passed through.

Thank you, I'm seeing you added an example. I've a small request to
hopefully make the example more useful: It currently uses only
“placeholder” parameters. It would be useful to also add one pre-filled
parameter to it. I would assume that this pre-filled parameter does not
appear within the Closure frame, but appears with the frame for the
actual function?

Updated the error dump example.

- How does it interact with `compact()`, specifically:

     $partial = compact(someValue(), ?);

Would the `?` be able to capture a variable containing the return value
of `someValue()`?

Good question! compact(), extract(), etc. can't actually work with PFA, because they operate on the ambient context, which PFA by design changes. I've updated the RFC to note three incompatible core functions. (The other is func_get_arg()).

--Larry Garfield

Hi

I'll give the RFC another read at a later point, but I wanted to get something out before I forget:

Am 2025-10-13 20:23, schrieb Larry Garfield:

- How does it interact with `compact()`, specifically:

     $partial = compact(someValue(), ?);

Would the `?` be able to capture a variable containing the return value
of `someValue()`?

Good question! compact(), extract(), etc. can't actually work with PFA, because they operate on the ambient context, which PFA by design changes. I've updated the RFC to note three incompatible core functions. (The other is func_get_arg()).

- `get_defined_vars()` should be added to the list (possibly more?). Or to make things easier: Just say everything that doesn't already work with FCC will not work with PFA either.
- What will happen with `assert(?)`? Assert is special in that it captures the AST at compile time to render it in the error message. So specifically:

     $x = assert(?);
     assert(false); // How will the AssertionError look like?

Best regards
Tim Düsterhus

On Mon, Oct 13, 2025, at 4:00 PM, Tim Düsterhus wrote:

Hi

I'll give the RFC another read at a later point, but I wanted to get
something out before I forget:

Am 2025-10-13 20:23, schrieb Larry Garfield:

- How does it interact with `compact()`, specifically:

     $partial = compact(someValue(), ?);

Would the `?` be able to capture a variable containing the return
value
of `someValue()`?

Good question! compact(), extract(), etc. can't actually work with
PFA, because they operate on the ambient context, which PFA by design
changes. I've updated the RFC to note three incompatible core
functions. (The other is func_get_arg()).

- `get_defined_vars()` should be added to the list (possibly more?). Or
to make things easier: Just say everything that doesn't already work
with FCC will not work with PFA either.
- What will happen with `assert(?)`? Assert is special in that it
captures the AST at compile time to render it in the error message. So
specifically:

     $x = assert(?);
     assert(false); // How will the AssertionError look like?

Best regards
Tim Düsterhus

I've added assert() and get_defined_vars() to the list, and relabeled it to be incomplete. I also added a note about FCC and it being the same list.

--Larry Garfield

Hi

Am 2025-10-13 20:23, schrieb Larry Garfield:

OK, there was a miscommunication between Arnaud and I. It is using the variable name, not "args" already. Just with no _ and 0-based. I've updated the RFC accordingly.

Thank you! I noticed a small typo in one of the updated examples:

$c = fn(int $i1, ?float $f3, Point $points0, Point $points`): string => things($i1, $f3, $points0, $points1);

The second points parameter is misnamed.

We talked a bit more, and decided to tighten the rules further. I've updated the RFC accordingly. Essentially, the first example is correct, the second has been changed.

If the underlying function is variadic, and ... is used in the PFA, then it will accept an arbitrary number of arguments. In any other case, only explicitly-specified arguments will be passed through.

The RFC still says:

// Whereas strictly speaking, a variadic placeholder is equivalent to this:
$f = foo(1, ...);
$f = fn(int $b, int $c): int => foo(1, $b, $c, $d, ....array_slice(func_get_args(), 3));

Did you miss updating that? This also makes me realize that this means that PFA will not actually be a strict superset of FCC, no? FCC always passes through all the extra arguments, whereas PFA will not (at least based on your reply).

Best regards
Tim Düsterhus

Hi

Am 2025-10-14 15:29, schrieb Larry Garfield:

I've added assert() and get_defined_vars() to the list, and relabeled it to be incomplete. I also added a note about FCC and it being the same list.

assert() *is* compatible with FCC:

     <?php
     $f = assert(...);
     $f(1 < 0);

This will print:

     Fatal error: Uncaught AssertionError in /tmp/test.php:3

without any stringified expression. I assume it will be the same with PFA then? Or will that print something like this:

     Uncaught AssertionError: assert(?) in /tmp/test.php:4

--------------------

Some more notes:

1.

The desugaring in the RFC shows non-static Closures only. This makes sense to me for simplicity. The RFC should however explicitly specify whether or not the resulting Closures will be static Closures or not (or which conditions need to folks for them to be static). This is important to know, because non-static Closures might keep objects alive for longer than necessary or expected.

2.

I am noticing that the error messages are quite inconsistent with existing error messages:

     not enough arguments or placeholders for application of stuff, 1 given and at least 4 expected"

vs

     json_encode() expects at least 1 argument, 0 given

So it should probably be:

     Partial application of json_encode() expects at least 1 argument or placeholder, 0 given

or something similar. Can you please double-check all the error messages in the implementation for consistency with existing error messages?

Best regards
Tim Düsterhus

Hi

Am 2025-10-13 19:00, schrieb Arnaud Le Blanc:

- How does it interact with
`ReflectionFunctionAbstract::getClosureUsedVariables()`?
- Same question for other scope introspection functionality (e.g. Xdebug).

getClosureUsedVariables() and other introspection functionality will
report any used var as usual. Used vars are named after the parameter
if there is no collision. But this should not be considered API as
this may change. For instance, I want to optimize-out used vars for
parameters that are pre-filled with a literal. It would be possible to
hide used vars of a PFA in Reflection.

Understood, thank you for the clarification. Please add this information to the RFC (ideally with examples of the possible cases, e.g. the passing of the `Closure` object). It's fine if you explicitly say “we consider this an implementation detail, but this is how it currently works”. This is also useful information to have if a user reports a bug in PFA that relates to this, so that we can tell them “what you are doing is not guaranteed to work, please don't”.

Best regards
Tim Düsterhus