On Thu, Dec 25, 2025, at 1:57 AM, Edmond Dantes wrote:
> Hello everyone!
>
> RFC https://wiki.php.net/rfc/true_async
>
> Here is the seventh version of the RFC, revised and expanded.
Notes, collected as i go:
- Most RFCs have an example in the introduction of what the proposed syntax
looks like in a small but meaningful example. Please include such, as it helps
"set the stage" and provide context for the rest of the RFC.
- Please break up the API Overview code block into several pieces by type to
make scrolling easier.
- As I've said before, I'd rather have keywords than functions for the main API.
- Please please do NOT have finally() be a function on its own.
current_coroutine()->finally() is sufficient. Having it on its own is going to
be too confusing with the finally keyword for exceptions. Can we find a better
name for it that isn't confusing with exceptions? onCompletion()?
What is the difference then between $coroutine->finally() and
register_shutdown_function(), when the current coroutine happens to be main?
- getSpawnFileAndLine() and getSpawnLocation() - It's non-obvious when they're
first mentioned what the difference is. I don't know if they're described
better further down, but they should have a docblock here to avoid confusion.
- getAwaitingInfo(): array - Hell no, no struct arrays. Make it a class.
- Remember, we now have PFA, so `spawn('sleep', 2)` can now be spelled
`spawn(sleep(2, ...))`, to make it available for IDE analysis. :-) (I'm not
sure what the implications of that are/should be for all the other "callable
and arg array" cases.) But at very least, please use `func_name(...)`, not a
string, in all examples. Function name strings should basically never be used
again. (Another argument in favor of keywords.)
- "The non-blocking version of the file_get_contents function is not part of
this RFC." - Wait, I'm confused. I though the entire point of this RFC was
that we don't need a user-exposed alternate version of file_get_contents(). It
just blocks or doesn't as needed. Now you're saying that's not the case? We
need to use the silly spawn() syntax or it will block our coroutine? That's a
no-go for any existing library that happens to have a file_get_contents() in
it, which would then block if it gets called inside a coroutine it doesn't
expect. If that's not the case, then this comment is highly misleading.
Either way, it's scary.
- "This means that multiple calls to select <Awaitable> may produce different
results. " - I don't believe a select keyword or function has been discussed to
this point, so this line is confusing.
- Completable interface section needs an example. I don't grok what it means
in practice. I also don't understand what Awaitable is for other than being a
parent of Completable. At least not yet by this point in the RFC.
- The examples seem very inconsistent. Some are using foo(...), some are using
strings, and the "await with cancellation" example is using a long-closure
syntax for reasons I do not fathom. The inconsistency is making it difficult
to get a sense for what typical usage will be like.
- "As soon as the $cancellation is triggered, execution is interrupted with an
exception " - What does triggered mean? When it starts? Ends? Something
else? What's a use case here other than sleep? This is all very unclear.
- In Fibers, in the basic example, "start() blocks until the fiber completes" -
I assume that means it blocks the calling coroutine? Or does it block the
whole system until it completes?
- I have no idea what it means for coroutines to be "symmetric". This is
stated as a thing, but I have no clue what that means. That makes much of the
Fibers section incomprehensible.
- I will have to defer to the existing async gurus (especially Aaron P of
AmPHP, who wrote the Fibers RFC) to determine if the FIber integration is
logical and reasonable. For me, it feels very complicated but that may be
necessary. I am not sure.
- "Active coroutines (running or suspended) are protected from collection while
they have pending operations" - If I'm reading that correctly, it means
`span(very_long_function(...))` will start a coroutine, but not assign it to a
variable. So I cannot track it or refer to it, but it will still run in the
background until it completes, at which point it will get GCed. Is that
correct?
- The FrankenPHP example linked to... doesn't seem to do anything to start a
coroutine. I don't know what it's supposed to be demonstrating other than
FrankenPHP itself.
- OK, now all the way at the bottom we get to an explanation of
getSpawnFileAndLine() vs getSpawnLocation(). I am not convinced we need both,
and the naming is highly confusing. More discussion to have here.
- Non-Async code can use Cancellation, but... what does that even mean or do?
Why would someone use it? This is sneaking in a 3rd error type without much
guidance. catch(Throwable) is surprisingly common (rightly or wrongly), so
this feels like a major pothole potential.
- Oh crap, Scopes are now a SEPARATE RFC, of similar length? See previous
"this thing is huge" comment... I do not have the bandwidth right now to
review that, but I feel like I have to in order to speak to this one
intelligently. I also haven't gotten to read any of the linked references, as
I have run out of time.
More general comments:
This seems mostly logical as a low-level API. As I have stated before, though,
I don't want a low-level API. I want a *safe* API. I am not convinced those
are compatible. And I certainly do not want to see the low-level API in one
release and then have to wait for the next release for the safer API. Having
pipes and PFA in separate releases is bad enough. :-)
That this allows for zombie coroutines or "lost" background coroutines to exist
is, to me, a major problem. I realize changing that means bringing scopes back
in, which makes it bigger, but... I don't want to hand footguns to a million
PHP developers with no async experience. I don't trust us with that kind of
power to not do something grossly stupid.
As I've said before, I don't like await, spawn, etc. being free-standing
functions rather than keywords. That is very limiting on the syntax they can
use. It also means, as the examples show, we get this pattern very often:
await(spawn($fn));
That's very clumsy. It introduces two layers of wrapped functions, which means
a lot of extra parentheses.
I... have no idea what would happen if one used pipes here, but that's also an
open question. I think it would be:
$result = $fn |> spawn(...) |> await(...);
I am not sure if I like that or not. But regardless, the "start coroutine and
block until it's done" operation is common enough that it needs a cleaner way
of writing it than two nested function calls. We can debate what exactly that
is, but we need it.
I would much rather have $cor->onComplete() than finally(). And absolutely no
global function by that name, which is way too easy to confuse with the
existing keyword.
All the use of inline function callbacks also, once again, demonstrates why our
current long-closure syntax is a problem. We need to fix that, for simple
ergonomics if nothing else. It makes this sort of code a pain in the ass.
--Larry Garfield