Commit d3672493cc
Changed files (1)
doc/langref.html.in
@@ -5970,54 +5970,25 @@ test "global assembly" {
{#header_close#}
{#header_open|Async Functions#}
<p>
- An async function is a function whose callsite is split into an {#syntax#}async{#endsyntax#} initiation,
- followed by an {#syntax#}await{#endsyntax#} completion.
- </p>
- <p>
- When you call a function, it creates a stack frame,
- and then the function runs until it reaches a return
- statement, and then the stack frame is destroyed.
- At the callsite, the next line of code does not run
- until the function returns.
- </p>
- <p>
- An async function is like a function, but it can be suspended
- and resumed any number of times, and then it must be
- explicitly destroyed. When an async function suspends, it
- returns to the resumer.
+ When a function is called, a frame is pushed to the stack,
+ the function runs until it reaches a return statement, and then the frame is popped from the stack.
+ At the callsite, the following code does not run until the function returns.
</p>
- {#header_open|Minimal Async Function Example#}
<p>
- Declare an async function with the {#syntax#}async{#endsyntax#} keyword.
- The expression in angle brackets must evaluate to a struct
- which has these fields:
- </p>
- <ul>
- <li>{#syntax#}allocFn: fn (self: *Allocator, byte_count: usize, alignment: u29) Error![]u8{#endsyntax#} - where {#syntax#}Error{#endsyntax#} can be any error set.</li>
- <li>{#syntax#}freeFn: fn (self: *Allocator, old_mem: []u8) void{#endsyntax#}</li>
- </ul>
- <p>
- You may notice that this corresponds to the {#syntax#}std.mem.Allocator{#endsyntax#} interface.
- This makes it convenient to integrate with existing allocators. Note, however,
- that the language feature does not depend on the standard library, and any struct which
- has these fields is allowed.
- </p>
- <p>
- Omitting the angle bracket expression when defining an async function makes
- the function generic. Zig will infer the allocator type when the async function is called.
- </p>
- <p>
- Call an async function with the {#syntax#}async{#endsyntax#} keyword. Here, the expression in angle brackets
- is a pointer to the allocator struct that the async function expects.
+ An async function is a function whose callsite is split into an {#syntax#}async{#endsyntax#} initiation,
+ followed by an {#syntax#}await{#endsyntax#} completion. Its frame is
+ provided explicitly by the caller, and it can be suspended and resumed any number of times.
</p>
<p>
- The result of an async function call is a {#syntax#}promise->T{#endsyntax#} type, where {#syntax#}T{#endsyntax#}
- is the return type of the async function. Once a promise has been created, it must be
- consumed with {#syntax#}await{#endsyntax#}:
+ Zig infers that a function is {#syntax#}async{#endsyntax#} when it observes that the function contains
+ a <strong>suspension point</strong>. Async functions can be called the same as normal functions. A
+ function call of an async function is a suspend point.
</p>
+ {#header_open|Suspend and Resume#}
<p>
- Async functions start executing when created, so in the following example, the entire
- TODO
+ At any point, a function may suspend itself. This causes control flow to
+ return to the callsite (in the case of the first suspension),
+ or resumer (in the case of subsequent suspensions).
</p>
{#code_begin|test#}
const std = @import("std");
@@ -6025,32 +5996,25 @@ const assert = std.debug.assert;
var x: i32 = 1;
-test "call an async function" {
- var frame = async simpleAsyncFn();
- comptime assert(@typeOf(frame) == @Frame(simpleAsyncFn));
+test "suspend with no resume" {
+ var frame = async func();
assert(x == 2);
}
-fn simpleAsyncFn() void {
+
+fn func() void {
x += 1;
suspend;
+ // This line is never reached because the suspend has no matching resume.
x += 1;
}
{#code_end#}
- {#header_close#}
- {#header_open|Suspend and Resume#}
<p>
- At any point, an async function may suspend itself. This causes control flow to
- return to the caller or resumer. The following code demonstrates where control flow
- goes:
- </p>
- <p>
- TODO another test example here
- </p>
- <p>
- When an async function suspends itself, it must be sure that it will be
- resumed somehow, for example by registering its promise handle
- in an event loop. Use a suspend capture block to gain access to the
- promise (TODO this is outdated):
+ In the same way that each allocation should have a corresponding free,
+ Each {#syntax#}suspend{#endsyntax#} should have a corresponding {#syntax#}resume{#endsyntax#}.
+ A <strong>suspend block</strong> allows a function to put a pointer to its own
+ frame somewhere, for example into an event loop, even if that action will perform a
+ {#syntax#}resume{#endsyntax#} operation on a different thread.
+ {#link|@frame#} provides access to the async function frame pointer.
</p>
{#code_begin|test#}
const std = @import("std");
@@ -6061,9 +6025,9 @@ var result = false;
test "async function suspend with block" {
_ = async testSuspendBlock();
- std.debug.assert(!result);
+ assert(!result);
resume the_frame;
- std.debug.assert(result);
+ assert(result);
}
fn testSuspendBlock() void {
@@ -6075,19 +6039,15 @@ fn testSuspendBlock() void {
}
{#code_end#}
<p>
- Every suspend point in an async function represents a point at which the async function
- could be destroyed. If that happens, {#syntax#}defer{#endsyntax#} expressions that are in
- scope are run, as well as {#syntax#}errdefer{#endsyntax#} expressions.
- </p>
- <p>
- {#link|Await#} counts as a suspend point.
+ {#syntax#}suspend{#endsyntax#} causes a function to be {#syntax#}async{#endsyntax#}.
</p>
+
{#header_open|Resuming from Suspend Blocks#}
<p>
Upon entering a {#syntax#}suspend{#endsyntax#} block, the async function is already considered
suspended, and can be resumed. For example, if you started another kernel thread,
- and had that thread call {#syntax#}resume{#endsyntax#} on the promise handle provided by the
- {#syntax#}suspend{#endsyntax#} block, the new thread would begin executing after the suspend
+ and had that thread call {#syntax#}resume{#endsyntax#} on the frame pointer provided by the
+ {#link|@frame#}, the new thread would begin executing after the suspend
block, while the old thread continued executing the suspend block.
</p>
<p>
@@ -6103,7 +6063,7 @@ test "resume from suspend" {
_ = async testResumeFromSuspend(&my_result);
std.debug.assert(my_result == 2);
}
-async fn testResumeFromSuspend(my_result: *i32) void {
+fn testResumeFromSuspend(my_result: *i32) void {
suspend {
resume @frame();
}
@@ -6113,32 +6073,59 @@ async fn testResumeFromSuspend(my_result: *i32) void {
}
{#code_end#}
<p>
- This is guaranteed to be a tail call, and therefore will not cause a new stack frame.
+ This is guaranteed to tail call, and therefore will not cause a new stack frame.
</p>
{#header_close#}
{#header_close#}
- {#header_open|Await#}
+
+ {#header_open|Async and Await#}
<p>
- The {#syntax#}await{#endsyntax#} keyword is used to coordinate with an async function's
- {#syntax#}return{#endsyntax#} statement.
+ In the same way that every {#syntax#}suspend{#endsyntax#} has a matching
+ {#syntax#}resume{#endsyntax#}, every {#syntax#}async{#endsyntax#} has a matching {#syntax#}await{#endsyntax#}.
</p>
+ {#code_begin|test#}
+const std = @import("std");
+const assert = std.debug.assert;
+
+test "async and await" {
+ // Here we have an exception where we do not match an async
+ // with an await. The test block is not async and so cannot
+ // have a suspend point in it.
+ // This is well-defined behavior, and everything is OK here.
+ // Note however that there would be no way to collect the
+ // return value of amain, if it were something other than void.
+ _ = async amain();
+}
+
+fn amain() void {
+ var frame = async func();
+ comptime assert(@typeOf(frame) == @Frame(func));
+
+ const ptr: anyframe->void = &frame;
+ const any_ptr: anyframe = ptr;
+
+ resume any_ptr;
+ await ptr;
+}
+
+fn func() void {
+ suspend;
+}
+ {#code_end#}
<p>
- {#syntax#}await{#endsyntax#} is valid only in an {#syntax#}async{#endsyntax#} function, and it takes
- as an operand a promise handle.
- If the async function associated with the promise handle has already returned,
- then {#syntax#}await{#endsyntax#} destroys the target async function, and gives the return value.
- Otherwise, {#syntax#}await{#endsyntax#} suspends the current async function, registering its
- promise handle with the target async function. It becomes the target async function's responsibility
- to have ensured that it will be resumed or destroyed. When the target async function reaches
- its return statement, it gives the return value to the awaiter, destroys itself, and then
- resumes the awaiter.
+ The {#syntax#}await{#endsyntax#} keyword is used to coordinate with an async function's
+ {#syntax#}return{#endsyntax#} statement.
</p>
<p>
- A frame handle must be consumed exactly once after it is created with {#syntax#}await{#endsyntax#}.
+ {#syntax#}await{#endsyntax#} is a suspend point, and takes as an operand anything that
+ implicitly casts to {#syntax#}anyframe->T{#endsyntax#}.
</p>
<p>
- {#syntax#}await{#endsyntax#} counts as a suspend point, and therefore at every {#syntax#}await{#endsyntax#},
- a async function can be potentially destroyed, which would run {#syntax#}defer{#endsyntax#} and {#syntax#}errdefer{#endsyntax#} expressions.
+ There is a common misconception that {#syntax#}await{#endsyntax#} resumes the target function.
+ It is the other way around: it suspends until the target function completes.
+ In the event that the target function has already completed, {#syntax#}await{#endsyntax#}
+ does not suspend; instead it copies the
+ return value directly from the target function's frame.
</p>
{#code_begin|test#}
const std = @import("std");
@@ -6156,14 +6143,14 @@ test "async function await" {
assert(final_result == 1234);
assert(std.mem.eql(u8, seq_points, "abcdefghi"));
}
-async fn amain() void {
+fn amain() void {
seq('b');
var f = async another();
seq('e');
final_result = await f;
seq('h');
}
-async fn another() i32 {
+fn another() i32 {
seq('c');
suspend {
seq('d');
@@ -6183,31 +6170,156 @@ fn seq(c: u8) void {
{#code_end#}
<p>
In general, {#syntax#}suspend{#endsyntax#} is lower level than {#syntax#}await{#endsyntax#}. Most application
- code will use only {#syntax#}async{#endsyntax#} and {#syntax#}await{#endsyntax#}, but event loop
- implementations will make use of {#syntax#}suspend{#endsyntax#} internally.
+ code will use only {#syntax#}async{#endsyntax#} and {#syntax#}await{#endsyntax#}, but event loop
+ implementations will make use of {#syntax#}suspend{#endsyntax#} internally.
</p>
{#header_close#}
- {#header_open|Open Issues#}
+
+ {#header_open|Async Function Example#}
<p>
- There are a few issues with async function that are considered unresolved. Best be aware of them,
- as the situation is likely to change before 1.0.0:
+ Putting all of this together, here is an example of typical
+ {#syntax#}async{#endsyntax#}/{#syntax#}await{#endsyntax#} usage:
+ </p>
+ {#code_begin|exe|async#}
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+pub fn main() void {
+ _ = async amainWrap();
+
+ // Typically we would use an event loop to manage resuming async functions,
+ // but in this example we hard code what the event loop would do,
+ // to make things deterministic.
+ resume global_file_frame;
+ resume global_download_frame;
+}
+
+fn amainWrap() void {
+ amain() catch |e| {
+ std.debug.warn("{}\n", e);
+ if (@errorReturnTrace()) |trace| {
+ std.debug.dumpStackTrace(trace.*);
+ }
+ std.process.exit(1);
+ };
+}
+
+fn amain() !void {
+ const allocator = std.heap.direct_allocator;
+ var download_frame = async fetchUrl(allocator, "https://example.com/");
+ var awaited_download_frame = false;
+ errdefer if (!awaited_download_frame) {
+ if (await download_frame) |r| allocator.free(r) else |_| {}
+ };
+
+ var file_frame = async readFile(allocator, "something.txt");
+ var awaited_file_frame = false;
+ errdefer if (!awaited_file_frame) {
+ if (await file_frame) |r| allocator.free(r) else |_| {}
+ };
+
+ awaited_file_frame = true;
+ const file_text = try await file_frame;
+ defer allocator.free(file_text);
+
+ awaited_download_frame = true;
+ const download_text = try await download_frame;
+ defer allocator.free(download_text);
+
+ std.debug.warn("download_text: {}\n", download_text);
+ std.debug.warn("file_text: {}\n", file_text);
+}
+
+var global_download_frame: anyframe = undefined;
+fn fetchUrl(allocator: *Allocator, url: []const u8) ![]u8 {
+ const result = try std.mem.dupe(allocator, u8, "this is the downloaded url contents");
+ errdefer allocator.free(result);
+ suspend {
+ global_download_frame = @frame();
+ }
+ std.debug.warn("fetchUrl returning\n");
+ return result;
+}
+
+var global_file_frame: anyframe = undefined;
+fn readFile(allocator: *Allocator, filename: []const u8) ![]u8 {
+ const result = try std.mem.dupe(allocator, u8, "this is the file contents");
+ errdefer allocator.free(result);
+ suspend {
+ global_file_frame = @frame();
+ }
+ std.debug.warn("readFile returning\n");
+ return result;
+}
+ {#code_end#}
+ <p>
+ Now we remove the {#syntax#}suspend{#endsyntax#} and {#syntax#}resume{#endsyntax#} code, and
+ observe the same behavior, with one tiny difference:
+ </p>
+ {#code_begin|exe|blocking#}
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+pub fn main() void {
+ _ = async amainWrap();
+}
+
+fn amainWrap() void {
+ amain() catch |e| {
+ std.debug.warn("{}\n", e);
+ if (@errorReturnTrace()) |trace| {
+ std.debug.dumpStackTrace(trace.*);
+ }
+ std.process.exit(1);
+ };
+}
+
+fn amain() !void {
+ const allocator = std.heap.direct_allocator;
+ var download_frame = async fetchUrl(allocator, "https://example.com/");
+ var awaited_download_frame = false;
+ errdefer if (!awaited_download_frame) {
+ if (await download_frame) |r| allocator.free(r) else |_| {}
+ };
+
+ var file_frame = async readFile(allocator, "something.txt");
+ var awaited_file_frame = false;
+ errdefer if (!awaited_file_frame) {
+ if (await file_frame) |r| allocator.free(r) else |_| {}
+ };
+
+ awaited_file_frame = true;
+ const file_text = try await file_frame;
+ defer allocator.free(file_text);
+
+ awaited_download_frame = true;
+ const download_text = try await download_frame;
+ defer allocator.free(download_text);
+
+ std.debug.warn("download_text: {}\n", download_text);
+ std.debug.warn("file_text: {}\n", file_text);
+}
+
+fn fetchUrl(allocator: *Allocator, url: []const u8) ![]u8 {
+ const result = try std.mem.dupe(allocator, u8, "this is the downloaded url contents");
+ errdefer allocator.free(result);
+ std.debug.warn("fetchUrl returning\n");
+ return result;
+}
+
+fn readFile(allocator: *Allocator, filename: []const u8) ![]u8 {
+ const result = try std.mem.dupe(allocator, u8, "this is the file contents");
+ errdefer allocator.free(result);
+ std.debug.warn("readFile returning\n");
+ return result;
+}
+ {#code_end#}
+ <p>
+ Previously, the {#syntax#}fetchUrl{#endsyntax#} and {#syntax#}readFile{#endsyntax#} functions suspended,
+ and were resumed in an order determined by the {#syntax#}main{#endsyntax#} function. Now,
+ since there are no suspend points, the order of the printed "... returning" messages
+ is determined by the order of {#syntax#}async{#endsyntax#} callsites.
</p>
- <ul>
- <li>Async functions have optimizations disabled - even in release modes - due to an
- <a href="https://github.com/ziglang/zig/issues/802">LLVM bug</a>.
- </li>
- <li>
- There are some situations where we can know statically that there will not be
- memory allocation failure, but Zig still forces us to handle it.
- TODO file an issue for this and link it here.
- </li>
- <li>
- Zig does not take advantage of LLVM's allocation elision optimization for
- async function. It crashed LLVM when I tried to do it the first time. This is
- related to the other 2 bullet points here. See
- <a href="https://github.com/ziglang/zig/issues/802">#802</a>.
- </li>
- </ul>
{#header_close#}
{#header_close#}
@@ -6265,6 +6377,49 @@ comptime {
Note: This function is deprecated. Use {#link|@typeInfo#} instead.
</p>
{#header_close#}
+
+ {#header_open|@asyncCall#}
+ <pre>{#syntax#}@asyncCall(frame_buffer: []u8, result_ptr, function_ptr, args: ...) anyframe->T{#endsyntax#}</pre>
+ <p>
+ {#syntax#}@asyncCall{#endsyntax#} performs an {#syntax#}async{#endsyntax#} call on a function pointer,
+ which may or may not be an {#link|async function|Async Functions#}.
+ </p>
+ <p>
+ The provided {#syntax#}frame_buffer{#endsyntax#} must be large enough to fit the entire function frame.
+ This size can be determined with {#link|@frameSize#}. To provide a too-small buffer
+ invokes safety-checked {#link|Undefined Behavior#}.
+ </p>
+ <p>
+ {#syntax#}result_ptr{#endsyntax#} is optional ({#link|null#} may be provided). If provided,
+ the function call will write its result directly to the result pointer, which will be available to
+ read after {#link|await|Async and Await#} completes. Any result location provided to
+ {#syntax#}await{#endsyntax#} will copy the result from {#syntax#}result_ptr{#endsyntax#}.
+ </p>
+ {#code_begin|test#}
+const std = @import("std");
+const assert = std.debug.assert;
+
+test "async fn pointer in a struct field" {
+ var data: i32 = 1;
+ const Foo = struct {
+ bar: async fn (*i32) void,
+ };
+ var foo = Foo{ .bar = func };
+ var bytes: [64]u8 = undefined;
+ const f = @asyncCall(&bytes, {}, foo.bar, &data);
+ assert(data == 2);
+ resume f;
+ assert(data == 4);
+}
+
+async fn func(y: *i32) void {
+ defer y.* += 2;
+ y.* += 1;
+ suspend;
+}
+ {#code_end#}
+ {#header_close#}
+
{#header_open|@atomicLoad#}
<pre>{#syntax#}@atomicLoad(comptime T: type, ptr: *const T, comptime ordering: builtin.AtomicOrder) T{#endsyntax#}</pre>
<p>
@@ -6855,6 +7010,44 @@ export fn @"A function name that is a complete sentence."() void {}
{#see_also|@intToFloat#}
{#header_close#}
+ {#header_open|@frame#}
+ <pre>{#syntax#}@frame() *@Frame(func){#endsyntax#}</pre>
+ <p>
+ This function returns a pointer to the frame for a given function. This type
+ can be {#link|implicitly cast|Implicit Casts#} to {#syntax#}anyframe->T{#endsyntax#} and
+ to {#syntax#}anyframe{#endsyntax#}, where {#syntax#}T{#endsyntax#} is the return type
+ of the function in scope.
+ </p>
+ <p>
+ This function does not mark a suspension point, but it does cause the function in scope
+ to become an {#link|async function|Async Functions#}.
+ </p>
+ {#header_close#}
+
+ {#header_open|@Frame#}
+ <pre>{#syntax#}@Frame(func: var) type{#endsyntax#}</pre>
+ <p>
+ This function returns the frame type of a function. This works for {#link|Async Functions#}
+ as well as any function without a specific calling convention.
+ </p>
+ <p>
+ This type is suitable to be used as the return type of {#link|async|Async and Await#} which
+ allows one to, for example, heap-allocate an async function frame:
+ </p>
+ {#code_begin|test#}
+const std = @import("std");
+
+test "heap allocated frame" {
+ const frame = try std.heap.direct_allocator.create(@Frame(func));
+ frame.* = async func();
+}
+
+fn func() void {
+ suspend;
+}
+ {#code_end#}
+ {#header_close#}
+
{#header_open|@frameAddress#}
<pre>{#syntax#}@frameAddress() usize{#endsyntax#}</pre>
<p>
@@ -6870,14 +7063,14 @@ export fn @"A function name that is a complete sentence."() void {}
</p>
{#header_close#}
- {#header_open|@handle#}
- <pre>{#syntax#}@handle(){#endsyntax#}</pre>
+ {#header_open|@frameSize#}
+ <pre>{#syntax#}@frameSize() usize{#endsyntax#}</pre>
<p>
- This function returns a {#syntax#}promise->T{#endsyntax#} type, where {#syntax#}T{#endsyntax#}
- is the return type of the async function in scope.
+ This is the same as {#syntax#}@sizeOf(@Frame(func)){#endsyntax#}, where {#syntax#}func{#endsyntax#}
+ may be runtime-known.
</p>
<p>
- This function is only valid within an async function scope.
+ This function is typically used in conjunction with {#link|@asyncCall#}.
</p>
{#header_close#}