assemblyscript icon indicating copy to clipboard operation
assemblyscript copied to clipboard

Support Async/Await

Open cromwellian opened this issue 5 years ago • 22 comments

A lot of existing typescript code relies on async/await. And while WASM doesn't have a concept of the JS event loop or generators, it seems like this could be supported in some form by first lowering to a style using switch/case like async/await leveling to ES3-style (See https://blog.mariusschulz.com/2016/12/09/typescript-2-1-async-await-for-es3-es5) in combination with an implementation of Promise in the runtime. Perhaps this code transformation could be copied from the TypeScript compiler, lowering it in the AST before compilation to WASM.

The resolve/reject functions would need to be exported to JS so that the JS could drive the resolution (e.g. Promise((resolve, reject) => {.... setTimeout(() => resolve()) }) would need to be rewritten by the developer so that resolve/reject are exported being passed through some kind of "AssemblyScriptPromise" class that can record when it's invoked and step the generator along. setTimeout is an external function which invokes the resolve)

Since interop with the web is invariably async, this would go a long way to reducing the impedance mismatch.

cromwellian avatar Dec 14 '18 01:12 cromwellian

Absolutely, async/await is something that we'd like to support eventually. One missing building block at this point seems to be closure support, because promises and timeouts usually involve functions that access variables from a parent scope, though it might be possible to work around this with globals. Might also be good to note that deferring an operation to let's say the network or the filesystem is something that only the host can do at this point, and this leads us to interoperability concerns with the host, where waiting for host-bindings, reference-types et al. might make sense.

dcodeIO avatar Dec 14 '18 09:12 dcodeIO

Is there a bug to track lambdas? It seems that this could be split into two or three useful partials:

  1. lambdas with no captures
  2. lambdas that capture by value (read-only, equivalent to Java or C++ captures)
  3. lambdas that capture by reference (JS lambdas, C++ [&] lambdas)

#1 seems straight forward, #2 seems straight forward if translated like Java or C++ does it with synthetic functor classes. #3 is a little more difficult since you'd need to promote a local to heap storage, but I think #1 and #2 probably capture most of the functional style of programming I see in JS and TS, and cases like #3 could be temporarily worked around by the developer by boxing the local into a holder object, and relying on support for case #2

cromwellian avatar Dec 18 '18 00:12 cromwellian

Something that might help here: Binaryen now has support for pausing and resuming wasm in the new "Bysyncify" feature. Basically you get some special functions to unwind/rewind the call stack, and then Bysyncify does everything else for you - that is, it will automatically rewrite all the wasm that needs to be rewritten so that it can save/restore locals and the call stack. This adds overhead as you'd expect, but it's surprisingly small both in size and speed (thanks to integration with the Binaryen optimizer).

Attached is a wip blogpost (not public yet) with more details, including complete examples in pure wasm and in JS+wasm, and benchmarks.

If you're interested to use this, let me know if I can help!

Bysyncify.pdf

kripken avatar Jul 05 '19 16:07 kripken

Thanks for the hint (and for making it possible in the first place ofc)! There are multiple things that I think can make use of the bysyncify pass, like

  • async/await once we have an idea how to design a Promise implementation and/or binding
  • Preliminary exception handling?
  • Re-implementations of fs.readFile etc. in context of https://github.com/AssemblyScript/assemblyscript/pull/708 (Edit: Well, that's actually just a callback)
  • Maybe more?

The most important at this point seems to get something-exception-handling up, but I haven't thought about how feasible this would be with bysyncify yet. Would you say that makes sense? Also, there is the mention of "using the option bysyncify-imports to Bysyncify" which I think is not yet possible with the C-API, but that's certainly not a blocker and can be introduced with a PR, if it turns out we need it.

Edit: Just a crazy idea, but if it turns out that bysyncify can help with preliminary exception handling, would it be possible to "polyfill" it on that basis? Like, make actual try/catch blocks, and it would downlevel with a pass?

dcodeIO avatar Jul 05 '19 22:07 dcodeIO

Yeah, we could add a pass to lower wasm exceptions into a polyfill basically. Bysyncify has some useful tools for that, like analyzing which calls would need to be instrumented, but the new pass wouldn't need to think about locals etc., so it should have even less overhead.

I believe @aheejin will work on exceptions in binaryen soon. After that's done we can add a lowering/polyfill pass, should be straightforward (I can do it, if no one else wants to).

kripken avatar Jul 06 '19 01:07 kripken

Nice, that would be super useful! In the meantime we can start thinking about Promises over here I guess :)

dcodeIO avatar Jul 06 '19 02:07 dcodeIO

Interesting article about stateless and stateful (fiber) coroutines: http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2018/p1364r0.pdf

MaxGraey avatar Oct 22 '19 14:10 MaxGraey

For async / await which import / export to/from host we could use binaryen's Asyncify, but for internal async / awayt just generate stateless coroutines or generators via FSM approach similar to Python, C# and facebook's regenerator

MaxGraey avatar Jan 01 '20 00:01 MaxGraey

@MaxGraey are there examples somewhere showing the unwind/rewind being used in Assemblyscript?

unicomp21 avatar Aug 16 '20 22:08 unicomp21

For host side I saw only this wrapper. In general, you might be inspired this blog post probably

MaxGraey avatar Aug 16 '20 22:08 MaxGraey

The co_await implementation in ASIO, could this be leveraged in emscripten? Which could then be leveraged by AssemblyScript?

unicomp21 avatar May 27 '21 13:05 unicomp21

https://github.com/chriskohlhoff/asio/issues/643

unicomp21 avatar May 27 '21 13:05 unicomp21

~~co_await~~ spawn + yield_context from boost's asio is stackful coroutines which required to explicit handle stack which not possible in WebAssembly without stack switch proposal or some kind of emulation via asyncify and some host's glue code which is not our philosophy. Btw ECMAScript uses stackless coroutines which also uses in C#, Rust and C++20 Coroutines

MaxGraey avatar May 27 '21 13:05 MaxGraey

C++20 Coroutines

Isn't co_await part of C++20 coroutines? Not sure I understand.

https://en.cppreference.com/w/cpp/language/coroutines

uses the co_await operator to suspend execution until resumed

Coroutines are stackless

Maybe we're getting confused w/ the C++ fiber api?

unicomp21 avatar May 27 '21 13:05 unicomp21

Isn't co_await part of C++20 coroutines? Not sure I understand.

I updated prev comment. I ment routines from asio like spawn and etc. co_await, co_yield and etc it's C++20 already

MaxGraey avatar May 27 '21 13:05 MaxGraey

Hey guys. Can we at least support the async await syntax? Now that visitor-as has some great support for generating ast nodes, it might be fun to start transforming that syntax.

jtenner avatar May 27 '21 22:05 jtenner

@MaxGraey @dcodeIO If I'm understanding correctly, we simply need to implement the interfaces required by the the clang compiler, to get stackless coroutines using co_await in emscripten, right? And starting w/ the asio implementation might be a fast way of getting there? This implementation could then be leveraged by assemblyscript for async/await?

https://github.com/chriskohlhoff/asio/blob/7fe18ba1b3e2bfddb2ef8dd83883b4545d8444bc/asio/src/examples/cpp17/coroutines_ts/refactored_echo_server.cpp#L29

Do we agree what's being said here is true? Or am I completely confused? https://github.com/emscripten-core/emscripten/issues/10991#issuecomment-619459313

unicomp21 avatar May 29 '21 11:05 unicomp21

The thinking is we could have a c++ microtask executor/scheduler, similar to javascript, which gets called/flushed periodically by a timer on the javascript side. In high performance cases it could be called by requestAnimationFrame. In doing things this way, assemblyscript could leverage clang/c++ co_await directly, right?

@kripken would this work?

unicomp21 avatar Jun 04 '21 10:06 unicomp21

I had no idea, co_await actually generates a state machine. Redpanda leverages this heavily. If this conversation comes out the way I hope, I'm wondering if assemblyscript could simply layer itself atop co_await?

https://github.com/WebAssembly/binaryen/issues/4351

unicomp21 avatar Nov 20 '21 14:11 unicomp21

BTW, Gor is the guy who created co_await.

unicomp21 avatar Nov 20 '21 14:11 unicomp21

My two cents, after working a lot with C++20 coros.

A stackful coroutine is properly not the way to go with async/await. It is much closer to the stackless nature of the C++20 coros.

But I don't think you'd want to support those either. I've just finished writing some asio python bindings (https://github.com/klemens-morgenstern/asio-py experimental stuff) that allow integration between asio awaitables (the C++20 coros) and python async functions. I was just looking to assemblyscript to see if I maybe can build something similar, i.e. an assembly-script asio-based runtime, e.g. as a light-weight node.js replacement (still looking for a fitting wasm runtime)

My recommendation would be to decouple async as much as possible from the actual implementation. I.e. I would envision this, if assemblyscript can take the return value into account like this:

import asio

async some_coro(sock : asio.ip.tcp.socke) : asio.awaitable<i32>
{
    const bytes_written = await sock.async_write("Hello world!");
    return bytes_written;
}

By the same token one could provide a Promise<> from some-other place:

import {promise} from "my-runtime";
async some_coro(...) : promise<i32>;

This would be modeled on the C++20 design, where the return type dictates the type of coroutine that'll be used. That way my node.js runtime could use the imports from node, while my asio solution would explicitly import another set of classes.

If that is not possible, there needs to be a runtime that picks up the coro and execute it's SM. Python with asyncio does this explicitly:

async def main():
     print('hello')
     await asyncio.sleep(1)
     print('world')

asyncio.run(main())

I think this model would work too, if we add an implicit runtime-function that is not baked into the assembly script itself:

async function foobar() : promise<i32>;

explicitly_scheduler_it_somewhere(foobar.bind()); // similar to the above python
foobar(); // calls special function, e.g. __implicitly_schedule_it_on_my_runtime

That would allow me to plug assemblyscript into another runtime, though I reckon it might introduce some additional steps to glue that into the nodejs runtime.

klemens-morgenstern avatar Apr 20 '22 02:04 klemens-morgenstern

所以 ,2023了要怎么做呢,有点不太理解 So, how to do it in 2023, a little bit ununderstood

yichengxian avatar Sep 08 '23 03:09 yichengxian