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

Reply via email to