| # Suspendable Functions (`async`, `async*` and `sync*`) | |
| This document describes the implementation of _suspendable_ functions (functions with `async`, | |
| `async*` or `sync*` modifier) in Dart VM. The execution of such functions can be suspended in | |
| the middle at `await`/`yield`/`yield*` and resumed afterwards. | |
| When suspending a function, its local execution state (local variables and temporaries) is saved | |
| and the control is returned to the caller of the suspended function. | |
| When resuming a function, its local execution state is restored and execution continues within | |
| the suspendable function from the point where it was suspended. | |
| In order to minimize code size, the implementation is built using a variety of _stubs_ - reusable | |
| snippets of machine code generated by the VM/AOT. | |
| The high-level Dart logic used to implement suspendable functions (such as managing | |
| Futures/Streams/Iterators) is factored into helper Dart methods in core library. | |
| The rest of the document is organized as follows: first, general mechanisms for implementation of | |
| suspendable functions are described. | |
| After that, `async`, `async*` and `sync*` implementations are outlined using the general | |
| mechanisms introduced before. | |
| # Building blocks common to all suspendable functions | |
| ## SuspendState objects | |
| SuspendState objects are allocated on the heap and encapsulate the saved state of a suspended | |
| function. When suspending a function, its local frame (including local variables, spill slots | |
| and expression stack) is copied from the stack to a SuspendState object on the heap. | |
| When resuming a function, the frame is recreated and copied back from the SuspendState object | |
| into the stack. | |
| SuspendState objects have variable size and keep frame in the "payload" following a few fixed | |
| fields. | |
| In addition to a stack frame, SuspendState records a PC in the code of the suspended function | |
| where execution was suspended and can be resumed. | |
| The PC is also used by GC to find a stack map and scan through the pointers in the copied frame. | |
| SuspendState object also holds data and callbacks specific to a particular kind of suspendable | |
| function. | |
| SuspendState object is allocated during the first suspension and can be reused for the subsequent | |
| suspensions of the same function. | |
| For the declaration of SuspendState see [object.h](https://github.com/dart-lang/sdk/blob/main/runtime/vm/object.h#:~:text=class%20SuspendState), | |
| UntaggedSuspendState is declared in [raw_object.h](https://github.com/dart-lang/sdk/blob/main/runtime/vm/raw_object.h#:~:text=class%20UntaggedSuspendState). | |
| There is also a corresponding Dart class `_SuspendState`, declared in [async_patch.dart](https://github.com/dart-lang/sdk/blob/main/sdk/lib/_internal/vm/lib/async_patch.dart#:~:text=class%20_SuspendState). | |
| It contains Dart methods which are used to customize implementation for a particular kind of | |
| suspendable function. | |
| ## Frame of a suspendable function | |
| Suspendable functions are never inlined into other functions, so their local state is not mixed | |
| with the state of their callers (but other functions may be inlined into them). | |
| In order to have a single contiguous region of memory to copy during suspend/resume, parameters of | |
| suspendable functions are always copied into the local frame in the function prologue (see uses of | |
| `Function::MakesCopyOfParameters()` predicate). | |
| In order to keep and reuse SuspendState object, each suspendable function has an artificial local | |
| variable `:suspend_state` (see uses of `ParsedFunction::suspend_state_var()`), which is always | |
| allocated at the fixed offset in frame. It occupies the first local variable slot | |
| (`SuspendState::kSuspendStateVarIndex`) in case of unoptimized code or the first spill slot | |
| in case of optimized code (see `FlowGraphAllocator::AllocateSpillSlotForSuspendState`). | |
| The fixed location helps to find this variable in various stubs and runtime. | |
| ## Prologue and InitSuspendableFunction stub | |
| At the very beginning of a suspendable function `null` is stored into `:suspend_state` variable. | |
| This guarantees that `:suspend_state` variable can be accessed any time by GC and exception | |
| handling. | |
| After checking bounds of type arguments and types of arguments, suspendable functions call | |
| InitSuspendableFunction stub. | |
| InitSuspendableFunction stub does the following: | |
| - It calls a static generic Dart method specific to a particular kind of suspendable function. | |
| The argument of the stub is passed as type arguments to that method. | |
| Dart method performs initialization specific to a particular kind of suspendable function | |
| (for example, it creates `_Future<T>()` for async functions). | |
| It returns the instance which is used as a function-specific data. | |
| - Stub puts the function-specific data to `:suspend_state` variable, where it can be found by | |
| Suspend or Return stubs later. | |
| ## Suspend stub | |
| Suspend stub is called from a suspendable function when its execution should be suspended. | |
| Suspend stub does the following: | |
| - It inspects `:suspend_state` variable and checks if it contains an instance of SuspendState. | |
| If it doesn't, then stub allocates a new instance with a payload sufficient to hold a frame of | |
| the suspendable function. The newly allocated SuspendState is stored into `:suspend_state` | |
| variable, and previous value of `:suspend_state` (coming from InitSuspendableFunction stub) is | |
| saved to `SuspendState.function_data`. | |
| - In JIT mode, size of the frame may vary over time - expression stack depth varies during | |
| execution of unoptimized code and frame size may change during deoptimization and OSR. | |
| In AOT mode size of the stack frame stays the same. | |
| So, if stub finds an existing SuspendState object in JIT mode, it also checks if its frame | |
| payload has a sufficient size to hold a frame of the suspendable function. If it is not | |
| large enough, suspend stub calls `AllocateSuspendState` runtime entry to allocate a larger | |
| SuspendState object. The same runtime entry is called for slow path when allocating | |
| SuspendState for the first time. | |
| - The return address from Suspend stub to the suspendable function is saved to `SuspendState.pc`. | |
| It will be used to resume execution later. | |
| - The contents of the stack frame of the suspendable function between FP and SP is copied into | |
| SuspendState. | |
| - Write barrier: if SuspendState object resides in the old generation, then | |
| EnsureRememberedAndMarkingDeferred runtime entry is called. | |
| - If implementation of particular kind of suspendable function uses a customized Dart method | |
| for the suspension, then that method is called. | |
| Suspend stub supports passing one argument to the customization method. | |
| The result of the method is returned back to the caller of the suspendable function - it's | |
| the result of the suspendable function. | |
| If such method is not used, then Suspend stub returns its argument (so suspendable function | |
| could customize its return value). | |
| - On architectures other than x64/ia32, the frame of the suspendable function is removed and | |
| stub returns directly to the caller of the suspendable function. | |
| On x64/ia32, in order to maintain call/return balance and avoid performance penalty, | |
| Suspend stub returns to the suspendable function which immediately returns to its caller. | |
| For more details see `StubCodeCompiler::GenerateSuspendStub` in [stub_code_compiler.cc](https://github.com/dart-lang/sdk/blob/main/runtime/vm/compiler/stub_code_compiler.cc#:~:text=StubCodeCompiler::GenerateSuspendStub). | |
| ## Resume stub | |
| Resume stub is tail-called from `_SuspendState._resume` recognized method (which is called | |
| from Dart helpers). It is used to resume execution of the previously suspended function. | |
| Resume stub does the following: | |
| - Allocates Dart frame on the stack, using `SuspendState.frame_size` to calculate its size. | |
| - Copies frame contents from SuspendState to the stack. | |
| - In JIT mode restores pool pointer (PP). | |
| - Checks for the following cases and calls ResumeFrame runtime entry if any of this is true: | |
| + If resuming with an exception. | |
| + In JIT mode, if Code of the suspendable function is disabled (deoptimized). | |
| + In JIT mode, if there is a resumption breakpoint set by debugger. | |
| - Otherwise, jumps to `SuspendState.pc` to resume execution of the suspended function. | |
| On x64/ia32 the continuation PC is adjusted by adding `SuspendStubABI::kResumePcDistance` | |
| to skip over the epilogue which immediately follows the Suspend stub call to maintain | |
| call/return balance. | |
| ResumeFrame runtime entry is called as if it was called from suspended function at continuation PC. | |
| It handles all corner cases by throwing an exception, lazy deoptimizing or calling into | |
| the debugger. | |
| For more details see `StubCodeCompiler::GenerateResumeStub` in [stub_code_compiler.cc](https://github.com/dart-lang/sdk/blob/main/runtime/vm/compiler/stub_code_compiler.cc#:~:text=StubCodeCompiler::GenerateResumeStub) | |
| and `ResumeFrame` in [runtime_entry.cc](https://github.com/dart-lang/sdk/blob/main/runtime/vm/runtime_entry.cc#:~:text=ResumeFrame). | |
| ## Return stub | |
| Suspendable functions can use Return stub if they need to do something when execution of | |
| a function ends (for example, complete a Future or close a Stream). In such a case, | |
| suspendable function jumps to the Return stub instead of returning. | |
| Return stub does the following: | |
| - Removes the frame of the suspendable function (as if function epilogue was executed). | |
| - Calls a Dart method specific to a particular kind of suspendable function. | |
| The customization method takes a value of `:suspend_state` variable and a return value | |
| passed from the body of the suspendable function to the stub. | |
| - The value returned from the customization method is used as the result of | |
| the suspendable function. | |
| For more details see `StubCodeCompiler::GenerateReturnStub` in [stub_code_compiler.cc](https://github.com/dart-lang/sdk/blob/main/runtime/vm/compiler/stub_code_compiler.cc#:~:text=StubCodeCompiler::GenerateReturnStub). | |
| ## Exception handling and AsyncExceptionHandler stub | |
| Certain kinds of suspendable functions (async and async*) may need to catch all thrown exceptions | |
| which are not caught within the function body, and perform certain actions (such as completing | |
| the Future with an error). | |
| This is implemented by setting `has_async_handler` bit on `ExceptionHandlers` object. | |
| When looking for an exception handler, runtime checks if this bit is set and uses | |
| AsyncExceptionHandler stub as a handler (see `StackFrame::FindExceptionHandler`). | |
| AsyncExceptionHandler stub does the following: | |
| - It inspects the value of `:suspend_state` variable. If it is `null` (meaning the prologue has not | |
| finished yet), the exception should not be handled and it is rethrown. | |
| This makes it possible for argument type checks to throw an exception synchronously | |
| instead of completing a Future with an error. | |
| - Otherwise, stub removes the frame of the suspendable function (as if function epilogue was | |
| executed) and calls `_SuspendState._handleException` Dart method. AsyncExceptionHandler stub | |
| does not use separate Dart helper methods for async and async* functions as exception handling is | |
| not performance sensitive and currently uses only one bit in `ExceptionHandlers` to select | |
| a stub handler for simplicity. | |
| - The value returned from `_SuspendState._handleException` is used as the result of the | |
| suspendable function. | |
| For more details see `StubCodeCompiler::GenerateAsyncExceptionHandlerStub` in [stub_code_compiler.cc](https://github.com/dart-lang/sdk/blob/main/runtime/vm/compiler/stub_code_compiler.cc#:~:text=StubCodeCompiler::GenerateAsyncExceptionHandlerStub). | |
| ## IL instructions | |
| When compiling suspendable functions, the following IL instructions are used: | |
| - `Call1ArgStub` instruction is used to call one-argument stubs such as InitSuspendableFunction. | |
| - `Suspend` instruction is used to call Suspend stub. After calling Suspend stub, | |
| on x64/ia32 it also generates an epilogue right after the stub, in order to | |
| return to the caller after suspending without disrupting call/return balance. | |
| Due to this extra epilogue following the Suspend stub call, the resumption PC is | |
| not the same as the return address of the Suspend stub. So `Suspend` instruction | |
| uses 2 distinct deopt ids for the Suspend stub call and resumption PC. | |
| - `Return` instruction jumps to a Return stub instead of returning for certain kinds | |
| of suspendable functions (async and async*). | |
| # Combining all pieces together | |
| ## Async functions | |
| See [async_patch.dart](https://github.com/dart-lang/sdk/blob/main/sdk/lib/_internal/vm/lib/async_patch.dart) for the corresponding Dart source code. | |
| Async functions use the following customized stubs: | |
| ### InitAsync stub | |
| InitAsync = InitSuspendableFunction stub which calls `_SuspendState._initAsync`. | |
| `_SuspendState._initAsync` creates a `_Future<T>` instance which is used as the result of | |
| the async function. This `_Future<T>` instance is kept in `:suspend_state` variable until | |
| `_SuspendState` instance is created during the first `await`, and then kept in | |
| `_SuspendState._functionData`. This instance is returned from `_SuspendState._await`, | |
| `_SuspendState._returnAsync`, `_SuspendState._returnAsyncNotFuture` and | |
| `_SuspendState._handleException` methods to serve as the result of the async function. | |
| ### Await stub | |
| Await = Suspend stub which calls `_SuspendState._await`. It implements the `await` expression. | |
| `_SuspendState._await` allocates 'then' and 'error' callback closures when called for | |
| the first time. These callback closures resume execution of the async function via Resume stub. | |
| It is possible to create callbacks eagerly in the InitAsync stub, but there is a significant | |
| fraction of async functions which don't have `await` at all, so creating callbacks lazily during | |
| the first `await` makes those functions more efficient. | |
| If an argument of `await` is a Future, then `_SuspendState._await` attaches 'then' and 'error' | |
| callbacks to that Future. Otherwise it schedules a micro-task to continue execution of | |
| the suspended function later. | |
| ### AwaitWithTypeCheck stub | |
| AwaitWithTypeCheck is a variant of Await stub which additionally passes type argument `T` | |
| and calls `_SuspendState._awaitWithTypeCheck` in order to test if the value has a | |
| correct `Future<T>` type before awaiting. | |
| This runtime check is needed to maintain soundness in case value is a Future of an | |
| incompatible type, for example: | |
| ``` | |
| final FutureOr<Object> f = Future<Object?>.value(null); | |
| Object x = await f; // x == f, not null. | |
| ``` | |
| ### ReturnAsync stub | |
| ReturnAsync stub = Return stub which calls `_SuspendState._returnAsync`. | |
| It is used to implement `return` statement (either explicit or implicit when reaching | |
| the end of function). | |
| `_SuspendState._returnAsync` completes `_Future<T>` which is used as the result of | |
| the async function. | |
| ### ReturnAsyncNotFuture stub | |
| ReturnAsyncNotFuture stub = Return stub which calls `_SuspendState._returnAsyncNotFuture`. | |
| ReturnAsyncNotFuture is similar to ReturnAsync, but used when compiler can prove that | |
| return value is not a Future. It bypasses the expensive `is Future` test. | |
| ### Execution flow in async functions | |
| The following diagram depicts how the control is passed in a typical async function: | |
| ``` | |
| Caller Future<T> foo() async Stubs Dart _SuspendState methods | |
| | | |
| *-------------------> | | |
| (prologue) -------------> InitAsync | |
| | | |
| *----------> _initAsync | |
| (creates _Future<T>) | |
| | <--------- | |
| | <-----------------------* | |
| | | |
| | | |
| (await) ----------------> AwaitAsync | |
| | | |
| *----------> _await | |
| (setups resumption) | |
| (returns _Future<T>) | |
| | <--------- | |
| | <---------------------------------------------* | |
| Awaited Future is completed | |
| | | |
| *------------------------------------------> Resume | |
| | | |
| (after await) <---------------* | |
| | | |
| | | |
| (return) ---------------> ReturnAsync/ReturnAsyncNotFuture | |
| | | |
| *----------> _returnAsync/_returnAsyncNotFuture | |
| (completes _Future<T>) | |
| (returns _Future<T>) | |
| | <--------- | |
| | <---------------------------------------------* | |
| ``` | |
| ## Async* functions | |
| See [async_patch.dart](https://github.com/dart-lang/sdk/blob/main/sdk/lib/_internal/vm/lib/async_patch.dart) | |
| for the corresponding Dart source code. | |
| Async* functions use the following customized stubs: | |
| ### InitAsyncStar stub | |
| InitAsyncStar = InitSuspendableFunction stub which calls `_SuspendState._initAsyncStar`. | |
| `_SuspendState._initAsyncStar` creates `_AsyncStarStreamController<T>` instance which is used | |
| to control the Stream returned from the async* function. `_AsyncStarStreamController<T>` is kept | |
| in `_SuspendState._functionData` (after the first suspension at the beginning of async* function). | |
| ## YieldAsyncStar stub and `yield`/`yield*` | |
| YieldAsyncStar = Suspend stub which calls `_SuspendState._yieldAsyncStar`. | |
| This stub is used to suspend async* function at the beginning (until listener is attached to | |
| the Stream returned from async* function), and at `yield` / `yield*` statements. | |
| When `_SuspendState._yieldAsyncStar` is called at the beginning of async* function it creates | |
| a callback closure to resume body of the async* function (via Resume stub), creates and | |
| returns `Stream`. | |
| `yield` / `yield*` statements are implemented in the following way: | |
| ``` | |
| _AsyncStarStreamController controller = :suspend_state._functionData; | |
| if (controller.add/addStream(<expr>)) { | |
| return; | |
| } | |
| if (YieldAsyncStar()) { | |
| return; | |
| } | |
| ``` | |
| `_AsyncStarStreamController.add`, `_AsyncStarStreamController.addStream` and YieldAsyncStar stub | |
| can return `true` to indicate that Stream doesn't have a listener anymore and execution of | |
| async* function should end. | |
| Note that YieldAsyncStar stub returns a value passed to a Resume stub when resuming async* | |
| function, so the 2nd hasListeners check happens right before the async* function is resumed. | |
| See `StreamingFlowGraphBuilder::BuildYieldStatement` for more details about `yield` / `yield*`. | |
| ### Await stub | |
| Async* functions use the same Await stub which is used by async functions. | |
| ### ReturnAsyncStar stub | |
| ReturnAsyncStar stub = Return stub which calls `_SuspendState._returnAsyncStar`. | |
| `_SuspendState._returnAsyncStar` closes the Stream. | |
| ### Execution flow in async* functions | |
| The following diagram depicts how the control is passed in a typical async* function: | |
| ``` | |
| Caller Stream<T> foo() async* Stubs Dart helper methods | |
| | | |
| *-------------------> | | |
| (prologue) -------------> InitAsyncStar | |
| | | |
| *----------> _SuspendState._initAsyncStar | |
| (creates _AsyncStarStreamController<T>) | |
| | <--------- | |
| | <-----------------------* | |
| * ------------------> YieldAsyncStar | |
| | | |
| *----------> _SuspendState._yieldAsyncStar | |
| (setups resumption) | |
| (returns _AsyncStarStreamController.stream) | |
| | <--------- | |
| | <---------------------------------------------* | |
| Stream is listened | |
| | | |
| *------------------------------------------> Resume | |
| | | |
| (after prologue) <--------------* | |
| | | |
| | | |
| (yield) --------------------------------> _AsyncStarStreamController.add | |
| (adds value to Stream) | |
| (checks if there are listeners) | |
| | <----------------------------------- | |
| * ------------------> YieldAsyncStar | |
| | | |
| *----------> _SuspendState._yieldAsyncStar | |
| | <--------- | |
| | <---------------------------------------------* | |
| Micro-task to run async* body | |
| | | |
| *----------------------------------------------------------> _AsyncStarStreamController.runBody | |
| (checks if there are listeners) | |
| Resume <------- | |
| | | |
| (after yield) <---------------* | |
| | | |
| | | |
| (return) ---------------> ReturnAsyncStar | |
| | | |
| *----------> _SuspendState._returnAsyncStar | |
| (closes _AsyncStarStreamController) | |
| | <--------- | |
| | <---------------------------------------------* | |
| ``` | |
| ## Sync* functions | |
| See [async_patch.dart](https://github.com/dart-lang/sdk/blob/main/sdk/lib/_internal/vm/lib/async_patch.dart) | |
| for the corresponding Dart source code. | |
| Sync* functions use the following customized stubs: | |
| ### InitSyncStar stub | |
| InitSyncStar = InitSuspendableFunction stub which calls `_SuspendState._initSyncStar`. | |
| `_SuspendState._initSyncStar` creates a `_SyncStarIterable<T>` instance which is returned | |
| from sync* function. | |
| ### SuspendSyncStarAtStart stub | |
| SuspendSyncStarAtStart = Suspend stub which calls `_SuspendState._suspendSyncStarAtStart`. | |
| This stub is used to suspend execution of sync* at the beginning. It is called after | |
| InitSyncStar in the sync* function prologue. The body of sync* function doesn't run | |
| until Iterator is not obtained from Iterable (`_SyncStarIterable<T>`) which is returned from | |
| the sync* function. | |
| ### CloneSuspendState stub | |
| This stub creates a copy of SuspendState object. It is used to clone state of sync* | |
| function (suspended at the beginning) for each Iterator instance obtained from | |
| Iterable. | |
| See `StubCodeCompiler::GenerateCloneSuspendStateStub`. | |
| ### SuspendSyncStarAtYield stub and `yield`/`yield*` | |
| SuspendSyncStarAtYield = Suspend stub which doesn't call helper Dart methods. | |
| SuspendSyncStarAtYield is used to implement `yield` / `yield*` statements in sync* functions. | |
| `yield` / `yield*` statements are implemented in the following way: | |
| ``` | |
| _SyncStarIterator iterator = :suspend_state._functionData; | |
| iterator._current = <expr>; // yield <expr> | |
| OR | |
| iterator._yieldStarIterable = <expr>; // yield* <expr> | |
| SuspendSyncStarAtYield(true); | |
| ``` | |
| See `StreamingFlowGraphBuilder::BuildYieldStatement` for more details about `yield` / `yield*`. | |
| The value passed to SuspendSyncStarAtYield is returned back from the invocation of | |
| Resume stub. `true` indicates that iteration can continue. | |
| ### Returning from sync* functions. | |
| Sync* function do not use Return stubs. Instead, return statements are rewritten to return `false` | |
| in order to indicate that iteration is finished. | |
| ### Execution flow in sync* functions | |
| The following diagram depicts how the control is passed in a typical sync* function: | |
| ``` | |
| Caller Iterable<T> foo() sync* Stubs Dart helpers | |
| | | |
| *-------------------> | | |
| (prologue) -------------> InitSyncStar | |
| | | |
| *----------> _SuspendState._initSyncStar | |
| (creates _SyncStarIterable<T>) | |
| | <--------- | |
| | <-----------------------* | |
| * ------------------> SuspendSyncStarAtStart | |
| | | |
| *----------> _SuspendState._suspendSyncStarAtStart | |
| (remembers _SuspendState at start) | |
| (returns _SyncStarIterable<T>) | |
| | <--------- | |
| | <---------------------------------------------* | |
| Iterable.iterator is called | |
| | | |
| *----------------------------------------------------------> _SyncStarIterable<T>.iterator | |
| (creates _SyncStarIterator<T>) | |
| | | |
| CloneSuspendState <-------* | |
| (makes a copy of _SuspendState at start) | |
| | | |
| *-----------> | | |
| | <------------------------------------------------------- (returns _SyncStarIterator<T>) | |
| Iterator.moveNext is called | |
| | | |
| *----------------------------------------------------------> _SyncStarIterator<T>.moveNext | |
| (iterates over the cached yield* iterator, if any) | |
| (resumes sync* body to get the next element) | |
| Resume <------- | |
| | | |
| (after prologue) <--------------* | |
| | | |
| | | |
| (yield) ---------------> SuspendSyncStarAtYield(true) | |
| | | |
| *----------> | |
| (the next element is cached in _SyncStarIterator<T>._current) | |
| (returns true indicating that the next element is available) | |
| | <---------------------------------------------------------- | |
| Iterator.moveNext is called | |
| | | |
| *----------------------------------------------------------> _SyncStarIterator<T>.moveNext | |
| (iterates over the cached yield* iterator, if any) | |
| (resumes sync* body to get the next element) | |
| Resume <------- | |
| | | |
| (after yield) <-----------------* | |
| | | |
| | | |
| (return false) -----------------------------> | |
| (returns false indicating that iteration is finished) | |
| | <---------------------------------------------------------- | |
| ``` |