JAWSM - a JavaScript to WASM compiler

Comment on: HN Reddit

About a year ago I open sourced a distributed load testing tool called Crows. It runs scenarios compiled to WebAssembly, and as much as I like writing Rust, I wanted to allow using a scripting language in Crows. There are some options to do that in WebAssembly, like using AssemblyScript, but there is no native support for any of the mainstream scripting languages like JavaScript or Python. And, for better or for worse, people using load testing tools are now used to being able to write them in JavaScript through the popularity of k6. While you can run JavaScript on top of WebAssembly today, it involves compiling a JavaScript engine to WASM first, and embedding it in the binary. While good tooling to do that exists, for example in a form of SterlingMonkey, it means having to ship a multi megabyte binary for each script, and pay the price of keeping all of that code in memory. In my rough testing with the Wasmtime runtime, each WebAssembly module generated by SterlingMonkey used about 10MBs of memory at the start. And while it may be OK in some scenarios, it is a no go in a lot of use cases.

That's why, about a half a year ago, I started working on a JS to WASM compiler called JAWSM. The idea is to compile scripts written in JS into WASM binaries, without relying on any existing engines, and thus to massively decrease the binary size and the memory required to run a script. When I started the project it was simply a way to learn more about recently standardized WASM proposals like WASM GC (garbage collected data structures) and exception handling. Soon after I started, I realized I'm making very good progress, so I pushed forward. Now, after less than 6 months of development, in my very limited free time, I have a proof of concept of all of the major JS semantics like scopes, async/await, exception handling, and generators, and the project passes about 25% of the Test262 test suite.

If you want to support further development of the project, please consider supporting me on GitHub.

Implementation details

JAWSM benefits from modern WASM proposals like WASM GC and exception handling. WASM core instructions are quite limited. You can read and write from memory, perform basic arithmetic functions, and call host provided functions, but also limited integer or float inputs and outputs. This makes compiling lower level code to WebAssembly fairly straightforward, but it also means any more advanced features have to be implemented on top of WebAssembly core. WASM GC changes the game by introducing basic data structures like structs and arrays, which are garbage collected when out of scope. This means that when implementing a garbage collected language on top of WASM GC, you don't have to reimplement the GC part. Of course WebAssembly semantics are vastly different from JavaScript semantics, but it is possible to emulate JavaScript behaviour using WASM. For example let's consider a JavaScript object. It has a prototype, own properties, may have a value (like objects wrappers). In WASM, it could be represented as a following struct:

(type $Object
  (struct
    (field $properties (mut (ref $PropertyMap)))
    (field $own_prototype (mut anyref))
    (field $value (mut anyref))))

Now we could have a function that sets a property on the object by modifying the $properties field or a function that fetches a property and checks the prototype if it can't find an own property. While relatively simple, this kind of implementation allows to implement most of the prototypal inheritance that JavaScript uses. Add a few specialized objects like $Function or $Array to the mix and you have a working language created on top of a WASM virtual machine.

Creating a JS to WASM compiler in this style involves a few steps:

  1. Creating objects and helpers required to implement JavaScript objects and semantics
  2. Creating a compiler that converts JavaScript instructions into WASM instructions. For example converting foo.bar = 10 would be translated into fetching a foo variable from the scope, creating an object holding a number 10, and calling a function to set a property named bar into the foo object's properties.
  3. Doing code transformations on generated WASM code in order to achieve things that are currently not supported in WASM, like for example stack switching

Once a significant portion of the language is implemented that way, it is possible to implement builting types and functions implementing them in JavaScript. For example, recently I've implemented a lot of the functions on the Array.prototype, like shift. The implementation is fully in JavaScript.

Future plans

I am at a point where the project has a good chance of eventually transitioning from the proof of concept stage into a production ready software. Sure, it will require a lot of work, and figuring out a few more non-trivial problems, but it looks very promising already. Something that I wouldn't think I would be able to say anytime soon.

As the funamentals, like JavaScript semantics, are now mostly working, I think that I will be able to gradually focus on simpler stuff and move forward with the builtin types and functions. I will also have to invest into tooling cause there are multiple parts of the project that are no longer cutting it at the scale the project reaches. Like, for example, most of the WASM code is generated using a Rust macro that has 10k lines of code and at the moment there is no way to split it into smaller pieces without changing the underlying implementation.

I'm looking forward to the future and I'm hoping the WebAssembly ecosystem will continue expanding as I think it's a very promising technology that still hasn't shown its' full potential.