1# Coroutines and promises 2The document describes single-threaded coroutine model. 3 4## Classes 5All the classes below are runtime classes. 6 7### Coroutine 8 9 10Coroutine class contains state and the associated Promise object (which will be fulfilled by the entrypoint method's return value). 11 12The state should contain information about coroutine's call stack and other neccessary information to suspend and resume execution. 13When runtime creates a new coroutine new stack is created and coroutine's entrypoint is called on the new stack. 14 15#### Fields 16* promise - the promise associated with the coroutine and returned by 'launch' instruction. 17* state - the coroutine's state needed to suspend and resume the coroutine. 18 19### JobQueue 20 21 22JobQueue interface is used to process promises asynchronously. 23When runtime executes `await` operator runtime puts the promise and the coroutine into the queue and suspends the coroutine. 24When the promise gets processed the waiting coroutine is scheduled again. 25 26### CoroutineManager 27 28 29CoroutineManager manages switching between coroutines. It responsible for suspending the current coroutine and choosing the next coroutine for execution. 30When control is returned from the coroutine entrypoint function, runtime calls `Terminate` for the coroutine. 31 32#### Fields 33* job_queue - an instance of JobQueue to process promises asynchronously. 34* wait_list - a list of waiting coroutines. 35* ready_to_run_list - the list of coroutines which can be scheduled if the current coroutine gets suspended. 36* running_coroutine - the running coroutine. 37 38#### Methods 39* `void Launch(Method entrypoint, List arguments)` 40 * Create an instance of a coroutine with empty stack 41 * Create a promise 42 * Suspend the current coroutine 43 * Put the current coroutine to the front `ready_to_run_list` 44 * Set `running_coroutine` to the new coroutine 45 * Invoke `entrypoint` on the new stack 46* `void Schedule()` 47 * Suspend the current coroutine 48 * Swap the head of `ready_to_run_list` and `current_coroutine` 49 * Resume `current_coroutine` 50* `void Unblock(Coroutine coro)` 51 * Remove the `coro` from the `wait_list` 52 * Put the coro to the front of `ready_to_run_list` 53* `void Await(Promise promise)` 54 * Suspend the current coroutine 55 * Put `promise` argument and the current coroutine to the `job_queue` 56 * Put the current coroutine to the end of `wait_list` 57 * Resume the coroutine from the front of `ready_to_run_list` 58* `void Terminate(Coroutine coro)` 59 * Resolve the promise by return value or reject it if there is an exception 60 * Delete coroutine `coro` 61 * Resume the coroutine from the front of `ready_to_run_list` 62* `void Suspend(Coroutine coro)` 63 * Save current execution state (hw registers, stack pointer, Thread structure) into the `coro`'s context 64* `void Resume(Coroutine coro)` 65 * Set the coroutine `coro` to `running_coroutine` 66 * Restore the coroutine state (hw registers, stack pointer, Thread structure) from coroutine's context 67 68## Coroutine awaiting 69The promise returned by `launch` instruction is used to await the coroutine. 70`await` function applied to the promise suspends execution of the current coroutine even if the promise is fulfilled. 71The promise is put into the JobQueue and the coroutine is put into `wait_list` until the promise gets processed. When the JobQueue processes the promise CoroutineManager schedules the coroutine for execution. 72 73## Coroutine life cycle 74Runtime launches a new coroutine when it executes `launch` instruction. The steps runtime executes are the following: 75 76* Create a new instance of Coroutine 77* Create a new Promise object and set it to the coroutine 78* Create new stack for the coroutine 79* Invoke entrypoint method on the new stack 80 81If the entrypoint method has `@MainThread` or `@Async` annotation the coroutine is always executed in the same thread where the instruction is executing. 82Else the coroutine may be executed in a different thread (implementation dependent). 83 84When the coroutine returns from the entrypoint method the return value is used to fulfill the Promise object if there is no pending exception. 85In case there is a pending exception, the Promise object is rejected and the exception is stored to the Promise object. 86 87The main coroutine is created in a special way during runtime initialization. Runtime doesn't create a new stack for it but uses the existing one. 88Entrypoint method for the main coroutine is specified in the command line arguments. 89 90 91 92During initialization runtime creates an instance of CoroutineManager and the main coroutine. The main coroutine starts executing `main` function. 93Suppose the main function executes a *launch foo()* instruction i.e. start an asynchronous operation. At this moment CoroutineManager creates an 94instance of coroutine and a Promise. Next CoroutineManager suspends the main coroutine and puts it to `ready_to_run_list`. 95CoroutineManager invokes new coroutine's entrypoint method *foo* on the new stack. 96After the coroutine finished its promise is automatically resolved by entrypoint's method return value. 97At this moment CoroutineManager schedules the next ready to run coroutine (the main coroutine). 98 99## Bytecode representation of async/await constructions 100Below is description of ETS constructions and the corresponding bytecode structures. 101 102| ETS operator | ETS bytecode | Comment | 103| :-- | :-- | :------ | 104|```async <function declaration>``` | ```.function @name@ <ets.annotation=ets.Async>``` | Frontend adds @Async runtime annotation if the source function is declared as `async`. | 105| `fn(args)` | `launch fn_method_id, args` | `launch` bytecode instruction is used to calls async functions. | 106| `await promise`; | `Coroutine.Await(promise)` | `await` is translated into call of the `await` native function implemented in runtime. | 107 108## Interaction with JS runtime 109#### JSMicroJobQueue 110Class JSMicroJobQueue, implementation of JobQueue interface, interacts with micro job queue in JS VM. 111Also this class should track associations between ETS promises and their counterparts in JS world. 112 113 114 115`promise_map` map stores references to ETS and JS Promise objects. The references should be GC roots for both GCs. 116If ETS GC moves an ETS promise it should update the reference in the map. The same is for JS GC. 117 118Implementation of `put` gets the associated JS instance of Promise (or creates a new one) and connects ETS promise to JS instance using `then` callback. 119When the callback is called (i.e. the promise is processed) the corresponding coroutine should be scheduled for execution. 120 121``` 122class JSMicroJobQueue: public JobInterface { 123public: 124 virtual void put(Promise promise, Coroutine coro) override { 125 auto js_env = GetJSEnv(); 126 // Get associated JS Promise instance 127 js_value js_promise = getJSPromise(promise); 128 if (js_promise == undefined) { 129 js_promise = js_env.create_promise(); 130 associate(promise, js_promise) 131 } 132 js_promise.then([&promise, &coro] (js_value value){ 133 promise.resolve(unwrap(value)); 134 CoroutineManager.Unblock(coro); 135 CoroutineManager.Schedule(); 136 }); 137 js_env.PushMicroTask(js_promise); 138 CoroutineManager.Await(coro, promise); 139 } 140}; 141``` 142 143#### Async function calling 144Calling an async function should launch a coroutine. 145In case JS engine calls an ETS function then control should pass through JS2ETS C++ bridge. 146The C++ bridge should check if the callee has @Async annotation and launch a new coroutine. 147Below is a pseudocode of such bridge. 148``` 149js_value JS2ETSEntrypoint(Napi::CallbackInfo *info) { 150 auto js_env = info->GetEnv(); 151 auto callee = info->GetCallee(); // js callee function object 152 ets_env ets = GetEtsEnv(); 153 // get name of function. 154 const char *method_name = js_env.GetProperty(fn, "name"); 155 // return the method which corresponds to the JS function 156 Method *method = ets_env.resolve(method_name); 157 if (method->IsAsync()) { // check @Async annotation 158 // for async function we start a coroutine 159 ets_value ets_promise = ets_env.launch(method, proxy(args)); 160 // wrap ETS promise (result) to JS promise 161 js_value js_promise = js_env.new_promise(); 162 JSMicroJobQueue.map(ets_promise, js_promise); 163 // connect ETS promise with JS promise 164 ets_promise.then([&js_promise] (ets_object value) { 165 js_promise.resolve(wrap(value)); 166 }); 167 ets_promise.catch([&js_promise] (ets_object value) { 168 js_promise.reject(wrap(value)); 169 }); 170 return js_promise; 171 } 172} 173``` 174 175#### Handling Promise objects returned from JS code 176If ETS code should await the Promise instance returned by a JS function the JS object should be wrapped into 177ETS object and the objects should be associated. 178Wrapping code should look as follow: 179 180``` 181ets_object JsPromiseToEtsPromise(js_value js_promise) { 182 js_env js_env = get_js_env(); 183 ets_env ets = get_ets_env(); 184 185 ets_object ets_promise = ets.create_promise(); 186 JSMicroJobQueue.map(ets_promise, js_promise); 187 return ets_promise; 188} 189``` 190 191## Example 192 193### TS function calls ETS function. 194Typescript source code: 195```typescript 196async function bar(): Promise<string> { 197 print("enter bar"); 198 return "exit bar"; 199} 200 201async function foo(): Promise<string> { 202 print("enter foo"); 203 print(await bar()); 204 return "exit foo"; 205} 206``` 207 208Translated ETS code is the same except `@Async` annotation is added to `foo` and `bar`. 209 210JS code executed by JS VM: 211```javascript 212async function main() { 213 print("enter main"); 214 print(await foo()); 215 print("exit main"); 216} 217 218main(); 219``` 220 221Expected output is: 222``` 223enter main 224enter foo 225enter bar 226exit bar 227exit foo 228exit main 229``` 230 231Execution and state: 232 233`>` - means already executed line 234`>>` - means the current line 235 236<table cellpadding=10 style="border-spacing: 0px"> 237<tr bgcolor="#ABB2B9"> 238<th>#</th> 239<th> 240Current function 241</th> 242<th>CoroutineManager's state<br>(before the current instruction)</th> 243<th>Output</th> 244<th>Comment</th> 245</tr> 246<tr> 247<td>1.</td> 248<td> 249<pre> 250>> function main() { 251 print("enter main"); 252 print(await foo()); 253 print("exit main"); 254 } 255</pre> 256</td> 257<td> 258running_coroutine=main<br> 259ready_to_run=[]<br> 260job_queue=[]<br> 261</td> 262<td></td> 263<td>There is only main coroutine exists.</td> 264</tr> 265<tr bgcolor="#EAECEE"> 266<td>2.</td> 267<td> 268<pre> 269> function main() { 270> print("enter main"); 271>> print(await >>foo()); 272 print("exit main"); 273 } 274</pre> 275</td> 276<td> 277running_coroutine=main<br> 278ready_to_run=[]<br> 279job_queue=[]<br> 280</td> 281<td>enter main</td> 282<td> 283`foo` is an ETS function. From JS point of view `foo` has native entrypoint `foo_impl`. 284</td> 285</tr> 286<tr> 287<td>3.</td> 288<td> 289<pre> 290> js_value foo_impl(Napi::CallbackInfo &info) { 291> auto js_env = info.GetEnv(); 292> auto callee = info.GetCallee(); // js callee function object (foo) 293> ets_env ets = GetEtsEnv(); 294> // get name of function. Should return "foo" 295> const char *method_name = js_env.GetProperty(fn, "name"); 296> // return the method which corresponds to "foo" function 297> Method *method = ets_env.resolve(method_name); 298> if (method->IsAsync()) { // check @Async annotation 299> // for async function we start a coroutine 300>> ets_value ets_promise = ets_env.launch(method, proxy(args)); 301 // wrap ETS promise (result) to JS promise 302 js_value js_promise = js_env.new_promise(); 303 // connect ETS promise with JS promise 304 ets_promise.then([&js_promise] (ets_object value) { 305 js_promise.resolve(wrap(value)); 306 }); 307 ets_promise.catch([&js_promise] (ets_object value) { 308 js_promise.reject(wrap(value)); 309 }); 310 return js_promise; 311 } 312 } 313</pre> 314</td> 315<td> 316running_coroutine=main<br> 317ready_to_run=[]<br> 318job_queue=[]<br> 319</td> 320<td> 321enter main 322</td> 323<td> 324`foo_impl` is a C++ bridge between JS and ETS. It converts JS values into ETS values, resolves the callee function. 325Since the callee is an async function the bridge creates a coroutine. 326</td> 327</tr> 328<tr bgcolor="#EAECEE"> 329<td>4.</td> 330<td> 331<pre> 332 @Async 333> function foo(): String { 334> print("enter foo"); 335>> print(await >>bar()); 336 return "exit foo"; 337 } 338</pre> 339</td> 340<td> 341running_coroutine=foo<br> 342ready_to_run=[main]<br> 343job_queue=[]<br> 344</td> 345<td> 346enter main<br> 347enter foo<br> 348</td> 349<td> 350Launching `foo` leads to put the `main` coroutine to the beginning of `ready_to_run_list`.<br> 351Since `bar` is an async function a coroutine will be created. 352</td> 353</tr> 354<tr> 355<td>5.</td> 356<td> 357<pre> 358 @Async 359> function bar(): String { 360> print("enter bar"); 361> return "exit bar"; 362>>} 363</pre> 364</td> 365<td> 366running_coroutine=bar<br> 367ready_to_run=[foo,main]<br> 368job_queue=[]<br> 369</td> 370<td> 371enter main<br> 372enter foo<br> 373enter bar<br> 374</td> 375<td> 376Launching `bar` coroutine ejects `foo` coroutine to the beginning of `ready_to_run_list`.<br> 377When `bar` is finised it resolves the associated promise and the coroutine gets terminated. 378Runtime resumes `foo` coroutine. 379</td> 380</tr> 381<tr bgcolor="#EAECEE"> 382<td>6.</td> 383<td> 384<pre> 385 @Async 386> function foo(): String { 387> print("enter foo"); 388>> print(>>await bar()); 389 return "exit foo"; 390 } 391</pre> 392</td> 393<td> 394running_coroutine=foo<br> 395ready_to_run=[main]<br> 396job_queue=[]<br> 397</td> 398<td> 399enter main<br> 400enter foo<br> 401enter bar<br> 402</td> 403<td> 404Awaiting `bar`'s promise leads to call of MicroJobQueue. 405</td> 406</tr> 407<tr> 408<td>7.</td> 409<td> 410<pre> 411> virtual void MicroJobQueue::put(Promise promise, Coroutine coro) override { 412> auto js_env = GetJSEnv(); 413> js_value js_promise = js_env.create_promise(); 414> js_promise.then([&promise, &coro] (js_value value){ 415 promise.resolve(unwrap(value)); 416 CoroutineManager.Unblock(coro); 417 CoroutineManager.Schedule(); 418> }); 419> js_env.PushMicroTask(js_promise); 420>> CoroutineManager.Await(coro, promise); 421 } 422</pre> 423</td> 424<td> 425running_coroutine=foo<br> 426ready_to_run=[main]<br> 427job_queue=[bar's promise]<br> 428</td> 429<td> 430enter main<br> 431enter foo<br> 432enter bar<br> 433</td> 434<td> 435The method creates a JS mirror of bar's promise and adds it to JS engine's MicroJobQueue.<br> 436`await` method will put `foo` coroutine to `wait_list`. The main coroutine will be resumed in `foo_impl`. 437</td> 438</tr> 439<tr bgcolor="#EAECEE"> 440<td>8.</td> 441<td> 442<pre> 443> js_value foo_impl(Napi::CallbackInfo &info) { 444> auto js_env = info.GetEnv(); 445> auto callee = info.GetCallee(); // js callee function object (foo) 446> ets_env ets = GetEtsEnv(); 447> // get name of function. Should return "foo" 448> const char *method_name = js_env.GetProperty(fn, "name"); 449> // return the method which corresponds to "foo" function 450> Method *method = ets_env.resolve(method_name); 451> if (method->IsAsync()) { // check @Async annotation 452> // for async function we start a coroutine 453> ets_value ets_promise = ets_env.launch(method, proxy(args)); 454> // wrap ETS promise (result) to JS promise 455> js_value js_promise = js_env.new_promise(); 456> // connect ETS promise with JS promise 457> ets_promise.then([&js_promise] (ets_object value) { 458 js_promise.resolve(wrap(value)); 459> }); 460> ets_promise.catch([&js_promise] (ets_object value) { 461 js_promise.reject(wrap(value)); 462> }); 463> return js_promise; 464 } 465 } 466</pre> 467</td> 468<td> 469running_coroutine=main<br> 470ready_to_run=[]<br> 471wait_list=[foo]<br> 472job_queue=[bar's promise]<br> 473</td> 474<td> 475enter main<br> 476enter foo<br> 477enter bar<br> 478</td> 479<td> 480Runtime resumes the main coroutine in `foo_impl`. The returned ETS promise is wrapped into JS promise and 481the promises are connected. I.e. resolving/rejecting of ETS promise leads to resolving/rejecting JS promise. 482</td> 483</tr> 484<tr> 485<td>9.</td> 486<td> 487<pre> 488> function main() { 489> print("enter main"); 490>> print(>>await foo()); 491 print("exit main"); 492 } 493</pre> 494</td> 495<td> 496running_coroutine=main<br> 497ready_to_run=[]<br> 498wait_list=[foo]<br> 499job_queue=[bar's promise]<br> 500</td> 501<td> 502enter main<br> 503enter foo<br> 504enter bar<br> 505</td> 506<td> 507JS runtime puts the promise returned by `foo` into job queue and gets suspended. 508</td> 509</tr> 510<tr bgcolor="#EAECEE"> 511<td>10.</td> 512<td> 513<pre> 514> main(); 515>> process_micro_job_queue(); 516</pre> 517</td> 518<td> 519running_coroutine=main<br> 520ready_to_run=[]<br> 521wait_list=[foo]<br> 522job_queue=[bar's promise,foo's promise]<br> 523</td> 524<td> 525enter main<br> 526enter foo<br> 527enter bar<br> 528</td> 529<td> 530When the main function is finished JS runtime starts processing micro job queue. 531The first promise in the queue is bar's promise. Since it is already resolved JS runtime 532calls its 'then' method which resumes `foo` coroutine. 533</td> 534</tr> 535<tr> 536<td>11.</td> 537<td> 538<pre> 539> virtual void MicroJobQueue::put(Promise promise, Coroutine coro) override { 540> auto js_env = GetJSEnv(); 541> js_value js_promise = js_env.create_promise(); 542> js_promise.then([&promise, &coro] (js_value value){ 543> promise.resolve(unwrap(value)); 544> CoroutineManager.Unblock(coro); 545>> CoroutineManager.Schedule(); 546> }); 547> js_env.PushMicroTask(js_promise); 548> CoroutineManager.Await(coro, promise); 549 } 550</pre> 551</td> 552<td> 553running_coroutine=main<br> 554ready_to_run=[foo]<br> 555wait_list=[]<br> 556job_que=[foo's promise]<br> 557</td> 558<td> 559enter main<br> 560enter foo<br> 561enter bar<br> 562</td> 563<td> 564MicroJobQueue calls `then` method for `bar` promise. The method schedules `foo` coroutine.<br> 565The main coroutine is ejected into `ready_to_run_list` and `foo` continues execution. 566</td> 567</tr> 568<tr bgcolor="#EAECEE"> 569<td>12.</td> 570<td> 571<pre> 572 @Async 573> function foo(): String { 574> print("enter foo"); 575> print(await bar()); 576> return "exit foo"; 577>>} 578</pre> 579</td> 580<td> 581running_coroutine=foo<br> 582ready_to_run=[main]<br> 583wait_list=[]<br> 584job_queue=[foo's promise]<br> 585</td> 586<td> 587enter main<br> 588enter foo<br> 589enter bar<br> 590exit bar<br> 591</td> 592<td> 593`foo` coroutine is terminated and runtime resolves its promise by the returned value. 594Then runtime resumes the main coroutine. 595</td> 596</tr> 597<tr> 598<td>13.</td> 599<td> 600<pre> 601> main(); 602>> process_micro_job_queue(); 603</pre> 604</td> 605<td> 606running_coroutine=main<br> 607ready_to_run=[]<br> 608wait_list=[]<br> 609job_queue=[foo's promise]<br> 610</td> 611<td> 612enter main<br> 613enter foo<br> 614enter bar<br> 615exit bar<br> 616</td> 617<td> 618JS engine continues processing micro job queue. The next task is foo's promise which already resolved. 619</td> 620</tr> 621<tr bgcolor="#EAECEE"> 622<td>14.</td> 623<td> 624<pre> 625> function main() { 626> print("enter main"); 627> print(await foo()); 628> print("exit main"); 629>> } 630</pre> 631</td> 632<td> 633running_coroutine=main<br> 634ready_to_run=[]<br> 635wait_list=[]<br> 636job_queue=[]<br> 637</td> 638<td> 639enter main<br> 640enter foo<br> 641enter bar<br> 642exit bar<br> 643exit foo<br> 644exit main<br> 645</td> 646<td> 647After processing foo's promise the main function gets resumed and finishes its execution. 648</td> 649</tr> 650<tr> 651</table> 652