by Marnix Klooster (https://github.com/marnix); license: public domain (attribution appreciated); status: probably-incorrect draft
(TODO: Update with Protty's feedback on gist revision 33 or 38 on Discord, https://discord.com/channels/605571803288698900/605572581046747136/794993176779292693 and subsequent discussion.)
(TODO: After I have a version that I'm at least a bit happy with, consider g-w1's suggestion of a pull request to https://github.com/ziglang/zig-spec/blob/disorganized-spec-chunks/spec/disorganized_facts_about_zig.md .)
How does async
(and the related suspend
/resume
and await
) work? Here is my attempt to describe this in a language specification kind of way.
At compile type, every fn
is categorized as being an async function or not. A function is async if its body contains a suspend
statement or await
expression, or a call to @frame()
, or a (normal non-nosuspend
) function call to an async function1.
A call to an async function can be marked with nosuspend
. This promises the compiler that the async function will never suspend at runtime, and it will be treated as a function call to a non-async function.
async f(...)
is only allowed on an async function f
. Anonymous storage for a frame for f
is allocated on the stack (so contained in the calling function's frame; so an async
call does not push a new frame onto the stack). Then control is passed to that frame as with a normal function call, until the function suspends (or the function completes normally, and the return value is stored in the frame). The anonymous frame storage remains valid until the end of the block containing the async
expression. Its contents are also returned (by value) by the async
expression.
@Frame(f)
is the type of a frame of function f
, i.e., it is the type of the value returned by async f(...)
. Importantly, just like with an async
call, the frame of an async function also contains the frame of each (normal non-nosuspend
) function call to an async function. (So a call from an async function to an async function does not push a new frame onto the stack.)
resume
takes a pointer to a frame, and passes control to the frame that it points to, until that function suspends again (or the function completes normally, and the return value is stored in the frame).
(A frame can be resumed using any copy of the frame contents, including using a pointer into the async
call's anonymous storage, as created by @frame()
.)
suspend
suspends the current async function (passing control back to the last resumer or else the async
caller). The 'suspend block' is executed before passing back control, but the function can already be resumed while the suspend block is still being executed. The function resumes after the suspend statement and block.
@frame()
returns a pointer to the current async function's frame. (It is usually called in a suspend
block, and the result is used in resume
.) [TODO: How does this interact with inline
, which presumably merges multiple frames into one?]
await
takes a frame (or anything that can be coerced to it), and then returns its return value if its function has completed, or otherwise it suspends the current async function (passing control back to the last resumer or else the async
caller), and on resume repeats the same steps.
Footnotes
-
Note that the Zig compiler internally desugars such calls to
await async f()
. Using that would make this specification simpler, but the current description helps me better in reading source code. ↩