diff --git a/doc/langref.html.in b/doc/langref.html.in index 0f964373c5..e02a406bd4 100644 --- a/doc/langref.html.in +++ b/doc/langref.html.in @@ -5970,54 +5970,25 @@ test "global assembly" { {#header_close#} {#header_open|Async Functions#}

+ 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. +

+

An async function is a function whose callsite is split into an {#syntax#}async{#endsyntax#} initiation, - followed by an {#syntax#}await{#endsyntax#} completion. + 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.

- 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. + Zig infers that a function is {#syntax#}async{#endsyntax#} when it observes that the function contains + a suspension point. Async functions can be called the same as normal functions. A + function call of an async function is a suspend point.

+ {#header_open|Suspend and Resume#}

- 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. -

- {#header_open|Minimal Async Function Example#} -

- Declare an async function with the {#syntax#}async{#endsyntax#} keyword. - The expression in angle brackets must evaluate to a struct - which has these fields: -

- -

- 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. -

-

- 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. -

-

- 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. -

-

- 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#}: -

-

- 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).

{#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#}

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

-

- TODO another test example here -

-

- 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 suspend block 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.

{#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#}

- 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. -

-

- {#link|Await#} counts as a suspend point. + {#syntax#}suspend{#endsyntax#} causes a function to be {#syntax#}async{#endsyntax#}.

+ {#header_open|Resuming from Suspend Blocks#}

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.

@@ -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#}

- 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.

{#header_close#} {#header_close#} - {#header_open|Await#} + + {#header_open|Async and Await#} +

+ 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#}. +

+ {#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#}

The {#syntax#}await{#endsyntax#} keyword is used to coordinate with an async function's - {#syntax#}return{#endsyntax#} statement. + {#syntax#}return{#endsyntax#} statement.

- {#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. + {#syntax#}await{#endsyntax#} is a suspend point, and takes as an operand anything that + implicitly casts to {#syntax#}anyframe->T{#endsyntax#}.

- A frame handle must be consumed exactly once after it is created with {#syntax#}await{#endsyntax#}. -

-

- {#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.

{#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#}

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.

{#header_close#} - {#header_open|Open Issues#} + + {#header_open|Async Function Example#}

- 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: +

+ {#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#} +

+ Now we remove the {#syntax#}suspend{#endsyntax#} and {#syntax#}resume{#endsyntax#} code, and + observe the same behavior, with one tiny difference: +

+ {#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#} +

+ 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.

- {#header_close#} {#header_close#} @@ -6265,6 +6377,49 @@ comptime { Note: This function is deprecated. Use {#link|@typeInfo#} instead.

{#header_close#} + + {#header_open|@asyncCall#} +
{#syntax#}@asyncCall(frame_buffer: []u8, result_ptr, function_ptr, args: ...) anyframe->T{#endsyntax#}
+

+ {#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#}. +

+

+ 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#}. +

+

+ {#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#}. +

+ {#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#}
{#syntax#}@atomicLoad(comptime T: type, ptr: *const T, comptime ordering: builtin.AtomicOrder) T{#endsyntax#}

@@ -6855,6 +7010,44 @@ export fn @"A function name that is a complete sentence."() void {} {#see_also|@intToFloat#} {#header_close#} + {#header_open|@frame#} +

{#syntax#}@frame() *@Frame(func){#endsyntax#}
+

+ 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. +

+

+ This function does not mark a suspension point, but it does cause the function in scope + to become an {#link|async function|Async Functions#}. +

+ {#header_close#} + + {#header_open|@Frame#} +
{#syntax#}@Frame(func: var) type{#endsyntax#}
+

+ 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. +

+

+ 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: +

+ {#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#}
{#syntax#}@frameAddress() usize{#endsyntax#}

@@ -6870,14 +7063,14 @@ export fn @"A function name that is a complete sentence."() void {}

{#header_close#} - {#header_open|@handle#} -
{#syntax#}@handle(){#endsyntax#}
+ {#header_open|@frameSize#} +
{#syntax#}@frameSize() usize{#endsyntax#}

- 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.

- This function is only valid within an async function scope. + This function is typically used in conjunction with {#link|@asyncCall#}.

{#header_close#}