Writing WebAssembly code in Haskell
In Asterius.Builtins, there are WebAssembly shims which serve as our runtime.
We choose to write WebAssembly code in Haskell, using Haskell as our familiar
meta-language.
As of now, there are two ways of writing WebAssembly code in Haskell. The first
way is directly manipulating AST types as specified in Asterius.Types. Those
types are pretty bare-metal and maps closely to binaryen IR. Simply write some
code to generate an AsteriusFunction, and ensure the function and its symbol
is present in the store when linking starts. It will eventually be bundled into
output WebAssembly binary file.
Directly using Asterius.Types is not a pleasant experience, it's basically a
DDoS on one's working memory, since the developer needs to keep a lot of things
in mind: parameter/local ids, block/loop labels, etc. Also, the resulting
Haskell code is pretty verbose, littered with syntactic noise (e.g. tons of
list concats when constructing a block)
We now provide an EDSL in Asterius.EDSL to construct an AsteriusFunction.
Its core type is EDSL a, and can be composed with a Monad or Monoid
interface. Most builtin functions in Asterius.Builtins are already refactored
to use this EDSL. Typical usages:
-
"Allocate" a parameter/local. Use
paramorlocalto obtain an immutableExpressionwhich corresponds to the value of a new parameter/local. There are also mutable variants. -
An opaque
LValtype is provided to uniformly deal with local reads/assignments and memory loads/stores. Once anLValis instantiated, it can be used to read anExpressionin the pure world, or set anExpressionin theEDSLmonad. -
Several side-effecting instructions can simply be composed with the monadic/monoidal interface, without the need to explicitly construct an anonymous block.
-
When we need named blocks/loops with branching instructions inside, use the
block/loopcombinators which has the type(Label -> EDSL ()) -> EDSL (). Inside the passed in continuation, we can usebreak'to perform branching. TheLabeltype is also opaque and cannot be inspected, the only thing we know is that it's scope-checked just like any ordinary Haskell value, so it's impossible to accidentally branch to an "inner" label.
The EDSL only checks for scope safety, so we don't mess up different locals or jump to non-existent labels. Type-safety is not guaranteed (binaryen validator checks for it anyway). Underneath it's just a shallow embedded DSL implemented with a plain old state monad. Some people call it the "remote monad design pattern".