On Sun, Nov 16, 2025, at 4:44 AM, Rob Landers wrote:
> On Sat, Nov 15, 2025, at 23:06, Edmond Dantes wrote:
>> > I guess my main thing is that this RFC should only cover coroutine machinery: it
>> > should not promise "transparent async" or "code
>>
>> It’s like trying to dig a hole with half a shovel :)
>>
>> > that works exactly the same" OR if it wants to make those claims, it should
>> > actually demonstrate
>> > how instead of hand-waving everything as an "implementation detail" when
>> > none of those claims can actually be validated without those details.
>>
>> All of my claims are backed by tests :)
>
> I will leave with some final advice. The problem with tests is that
> they only validate the current implementation, which isn’t guaranteed
> to be the final implementation. I would recommend reviewing your tests
> and matching up each of them to where you mention that behavior or
> define it in the RFC. If the tests are implementation-specific, then it
> needs to be defined in the RFC. For example, you say that the scheduler
> is 100% an implementation detail, but your outputs in the tests rely on
> a specific ordered queue. You should at least define the ordering the
> queue should be processed in the RFC (LIFO vs FIFO) so that even if the
> implementation changes, the tests still pass.
>
> That’s one example, you can review my previous comments to discover
> other examples, such as defining the rules of suspension points.
>
> I wish you the best,
A few thoughts to add:
First: People, please don't include me on the reply line. I just got 30 messages doubled.
Once or twice, fine, but somewhere in that thread someone should have trimmed the To field back to
just the list. Or use reply-list, just once. If nothing else, please remove *me* from the name
list. %3C/rant>
Second: I think the key point here is one that Kevlin Henny has raised before in presentations: If
you have any sort of concurrency, shared mutable state is a problem. Shared immutable state is no
problem. Unshared mutable state is no problem. Unshared immutable state is no problem. Shared
mutable state is where the problem is, and if you eliminate that, you eliminate virtually all race
conditions. (See also: the entire design of Rust). Naturally, easier said than done.
While I presume the people who frequent this list are disproportionately the sort that already try
to avoid shared mutable state on principle (I do), we are not a representative sample. So "the
code runs exactly the same, modulo any shared mutable state, there be dragons" (Edmond's
point) is true, but given the state of the PHP ecosystem also means "There's therefore an
unknown but probably very not-small number of dragons out there, just waiting for us" (Rob and
John's point, among others).
"This hasn't been a problem in practice for Swoole", I will take at face value as
true (I have no data on the matter); the degree to which that is indicative of the rest of the
ecosystem is what is highly debatable; Swoole et al represent a tiny fraction of the ecosystem, and
is used by people that know they're using it. Or they're using code written by top-notch
developers who have been preparing for this sort of scenario for a decade (Laminas, Symfony, etc.).
The degree to which we can apply that lack of concern to the billions of other lines of PHP code out
there is an open question.
So the debate seems to be between "let's assume it's safe and make the
nicer-to-work-with option" and "let's assume it's unsafe and so we need another
option." But Javascript-style colored functions would suck, for well-documented reasons. I
recall a very unpleasant experience where I had a perfectly fine sync function in JS that I needed
to modify to include an IO call, which required rewriting 5 functions and touching a dozen more.
That's what I think we all want to NOT have in PHP.
Third: So my question would be, is there a practical middle-ground? Is there a syntax and semantics
we could define that would cover *most* edge cases existing code may have involving shared mutable
state automatically, and have some manual way of handling the rest that doesn't require
updating 17 functions, some of which may not even be my code? Would that be "safe
enough"? Is there a "safe enough"?
I am still on team "structured concurrency only or bust" myself, though I admit that
doesn't fully resolve the issue. But the particular question here is how to indicate where
suspension may occur; the RFC right now says "any IO operation, implicitly, hope that's
OK." Which is both very convenient and potentially dangerous. But the status quo today of
"no IO operations, which means you have to write your own IO libraries from the socket level
up, GFL" is, obviously, not a great time.
Just completely spitballing, so possibly awful idea but maybe it will make someone else think of a
better one: Colored functions to opt-in to suspension but do NOT propagate that dependency up.
Something like, if you prefix a function call with await or async, then
you could suspend there; you're explicitly telling the engine you're OK with suspending
here if it wants to. If there's no other coroutines active it behaves exactly (and in this
case, actually exactly) as it would today, so there's no cost to add it. It would also work on
non-IO functions, so long-running CPU-bound processes could opt-in to "hey, I'm taking a
long time, if you want to jump in here that's safe." However, you're NOT limited to
using that keyword inside a marked function. You can use it on any call to file_get_contents().
However, a function can also declare itself sync, which will override that opt-in for
the entire call stack down. Basically it disables the suspend operation. So if you have a tricky
bit of global-using code, you can opt it out of switching without having to rewrite the world.
In code, it would look something like:
function log(string $m) {
// We're explicitly *allowing* the scheduler to swap out here,
// but it doesn't have to.
async file_put_contents('log.txt', $m, FILE_APPEND);
}
function first() {
// Do work.
// This call may end up switching to another coroutine for a while, or not, we don't care.
// Importantly, we don't need to "color" first() for it to work.
log('Did work');
}
sync function second() {
// Nothing inside here will swap to another coroutine, even though log()
// contains an async call inside it. The code behaves as if that
// keyword is just not there.
log('starting work.');
// Do work.
log ('finished work.");
}
This would mean that IO suspension becomes opt-in by default, so most IO-heavy libraries are not
going to benefit from any async until they're updated. But that seems it may be safer than
just assuming they're safe or else "oops, you're screwed on 8.6 until you add some
new 8.6-specific syntax, sorry." (Being able to write code that runs correctly in two, ideally
several, versions is crucially important for BC.)
As I said, this is just me spitballing so there may be a very good reason why it can't work;
I'm just trying to brainstorm a way out of the devil's dichotomy of "you have to
rewrite all of your code to have no shared mutable state, even though you don't know what
shared means, sorry" vs "your code may work, probably, but we're not sure, there
could be subtle bugs that are hard to find, sorry." Neither of which seems like good options.
--Larry Garfield