[php-src] PR #24623: [Draft] Experimental async/await syntax on top of Fibers
| From: | Eng3bbas | Date: | Fri, 23 Oct 2026 19:08:51 +0000 |
| Subject: | [php-src] PR #24623: [Draft] Experimental async/await syntax on top of Fibers | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-39786@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/24623
Author: Eng3bbas
## Summary
This adds a working **prototype** of
async/await to the Zend Engine, built
on the existing Fiber implementation. It's opened as a **draft** to start discussion; I know
language changes need an RFC and a vote, and this isn't meant to be merged as-is.
```php
use function Async\delay;
async function fetch(string $name, float $secs): string {
await delay($secs);
return "$name done";
}
$a = fetch("a", 0.2); // starts immediately, returns Async\Future
$b = fetch("b", 0.2);
echo await $a, await $b; // ~0.2s total, not 0.4s
```
### Syntax
- async function, async methods (public async function), async
function () use (...) {}, async fn() => ..., and static
variants.
- await <expr>: a unary expression.
- async is **contextual**: it's only a keyword before
function/fn, so userland async() functions keep working.
- await is a **reserved keyword**. It's still allowed as a method, constant or
property name, and namespaced names like Amp\Future\await() still lex as one token.
### Semantics
- Calling an async function runs its body in a new Fiber and returns an
Async\Future. The task starts eagerly and runs until it awaits a Future that
hasn't completed yet.
- await inside a task suspends that task. Outside a task (top-level script or a plain
Fiber), await drives the scheduler until the Future completes: ready tasks
resume FIFO, and when nothing is ready it sleeps until the next timer.
- Exceptions propagate through await. A Future can be awaited more than once.
- Deadlocks, and a task awaiting its own Future, throw an Error instead of hanging.
- Tasks still pending at shutdown are destroyed like suspended Fibers, so finally
blocks run.
### Runtime API (Zend/zend_async.stub.php)
- final class Async\Future { isDone(): bool }: not constructible, cloneable or
serializable.
- Async\async(callable $callback, mixed ...$args): Future
- Async\wait(Future $future): mixed: the function form of await. It
can't be named await because that's now a keyword.
- Async\delay(float $seconds): Future
- Async\suspend(): void: yields to other ready tasks.
## Implementation
Everything is desugared in the parser, so there are **no new opcodes and no VM changes**:
```php
async function f($a): T { body }
// becomes
function f($a): \Async\Future { return \Async\async(function () use (&$a): T { body }); }
async fn($a): T => expr // becomes fn($a): \Async\Future => \Async\async(fn(): T =>
expr)
await expr // becomes \Async\wait(expr)
```
- Zend/zend_language_scanner.l / zend_language_parser.y:
T_ASYNC, T_AWAIT and the grammar rules.
- Zend/zend_async.c: AST helpers plus the scheduler (ready queue, timers, fiber→task
map, GC handler).
- Compile-time errors: async abstract/body-less methods,
__construct/__destruct/__clone, by-reference returns, and
generators.
- Tokenizer data has been regenerated (T_ASYNC, T_AWAIT).
## Known limitations / open questions
- func_get_args() inside an async function body returns [], because the
body runs in an inner closure.
- Stack traces show an extra {closure} frame.
- Reflection reports the return type as Async\Future. The declared T
applies to the inner task.
- Making await reserved is a BC break for unqualified functions named
await (and tools such as nikic/PHP-Parser need to learn the new token).
- There's no I/O integration: only timers (delay) and cooperative
suspend(). A real proposal would need an event-loop or reactor story. See the separate
*True Async* RFC.
## Test plan
- [x] 17 new tests in Zend/tests/async/ cover basics, concurrency, exceptions, methods,
closures, arrow functions, by-ref params, contextual async, suspend(),
deadlock detection, compile errors, delay() validation, shutdown cleanup,
exit() inside a task, GC cycles, tokens, and awaiting from a plain Fiber.
- [x] Those tests pass on a debug build (leak checks on), with and without
opcache.enable_cli=1 opcache.jit=tracing.
- [x] Full run-tests.php on a --disable-all --enable-tokenizer debug build
on macOS arm64: no regressions against the unmodified baseline. The only existing test that changed
is ext/reflection/tests/bug74454.phpt, whose parse-error message now lists
"async".
- [ ] Not built or tested on Windows or ZTS.
🤖 Generated with [Claude Code](https://claude.com/claude-code)