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