Hi internals,
I'd like to gauge interest in adding native async/await syntax to PHP,
built on top of the Fiber implementation we've had since 8.1.
Fibers made it possible to write concurrent code without "function
colouring", but in practice everyone still picks a userland library
(Amp, ReactPHP, Revolt) together with its own Promise/Future type, and
the code doesn't say directly what it means. The idea is to give the
engine a small, standard vocabulary for this:
async function fetch(string $name, float $secs): string {
await Async\delay($secs);
return "$name done";
}
$a = fetch("a", 0.2); // starts right away, returns Async\Future
$b = fetch("b", 0.2);
echo await $a, await $b; // ~0.2s total, not 0.4s
Proposed semantics, in short:
- `async` can be applied to functions, methods, closures and arrow
functions. Calling one runs its body in a new Fiber and returns an
Async\Future. The task starts eagerly and runs until it awaits a
Future that isn't complete yet.
- `await <expr>` inside a task suspends that task. Outside a task (at
the top level, or in a plain Fiber) it drives a small scheduler until
the Future completes. It returns the value or rethrows the exception.
- `async` is a contextual keyword: it is only a keyword when followed
by `function` or `fn`, so existing userland async() functions keep
working. `await` would become a reserved keyword, though it remains
usable as a method, constant or property name.
- Tasks still pending at shutdown are destroyed the same way suspended
Fibers are, so their finally blocks run. Deadlocks throw an Error
instead of hanging.
- A minimal runtime API: final class Async\Future, Async\async(),
Async\wait() (the function form of await), Async\delay() and
Async\suspend().
I have a working prototype. Both constructs are desugared in the
parser into calls to the runtime functions, so there are no new opcodes
and no VM changes:
https://github.com/php/php-src/pull/24623
It comes with tests and passes the existing test suite on a debug
build (macOS arm64, NTS). I haven't tried it on Windows or ZTS yet.
Questions I'd especially like feedback on:
1. Is reserving `await` an acceptable BC break? Unqualified functions
named await() would break; namespaced calls such as
Amp\Future\await() would not.
2. How should this relate to the True Async RFC? Should syntax like
this be layered on top of that work rather than proposed separately?
3. What should I/O integration look like? The prototype only supports
timers and cooperative yielding. Real-world usefulness depends on
an event-loop or reactor story for streams and sockets.
4. Known rough edges in the desugaring approach: func_get_args() inside
an async body returns [], stack traces show an extra {closure} frame,
and reflection reports the return type as Async\Future. Are these
acceptable, or is a dedicated engine-level implementation preferred?
If there is interest, I'm happy to turn this into a proper RFC.
Thanks,
Abbas Al-Khateeb