Skip to content

[Draft] Experimental async/await syntax on top of Fibers - #24623

Draft
Eng3bbas wants to merge 1 commit into
php:masterfrom
Eng3bbas:feature/async-await
Draft

Eng3bbas wants to merge 1 commit into
php:masterfrom
Eng3bbas:feature/async-await

Conversation

@Eng3bbas

Copy link
Copy Markdown

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.

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:

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

  • 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.
  • Those tests pass on a debug build (leak checks on), with and without opcache.enable_cli=1 opcache.jit=tracing.
  • 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

Introduce `async function` / `async fn` (functions, methods, closures and
arrow functions) and an `await` expression, backed by a small Fiber-based
scheduler in Zend/zend_async.c.

* Calling an async function runs its body in a new Fiber and returns an
  Async\Future. The task starts eagerly and runs until its first await on
  a pending Future.
* `await $future` inside a task suspends the task; outside of a task it
  drives the scheduler until the Future completes. The Future's value is
  returned, or its exception rethrown.
* New API: final class Async\Future (isDone()), Async\async(),
  Async\wait() (the function form of `await`), Async\delay() and
  Async\suspend().
* `async` is a contextual keyword: it is only a keyword when followed by
  "function" or "fn", so async() remains usable as a function name.
  `await` is a new reserved keyword that remains usable as a member name.
* Both constructs are desugared in the parser:
    async function f($a): T { body }
  becomes
    function f($a): \Async\Future {
        return \Async\async(function () use (&$a): T { body });
    }
  and `await expr` becomes `\Async\wait(expr)`, so no new opcodes are needed.

This is a prototype for discussion and not an RFC implementation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Eng3bbas

Copy link
Copy Markdown
Author

Related feature-request issue for discussion: #24624

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants