Commit d3672493cc

Andrew Kelley <andrew@ziglang.org>
2019-08-15 22:46:43
basic docs for new async/await semantics
1 parent 55f5cee
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#}