[php-src] PR #24623: [Draft] Experimental async/await syntax on top of Fibers

From: 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)

« previous php.git-pulls (#39786) next »