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
param
orlocal
to obtain an immutableExpression
which corresponds to the value of a new parameter/local. There are also mutable variants. -
An opaque
LVal
type is provided to uniformly deal with local reads/assignments and memory loads/stores. Once anLVal
is instantiated, it can be used to read anExpression
in the pure world, or set anExpression
in theEDSL
monad. -
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
/loop
combinators which has the type(Label -> EDSL ()) -> EDSL ()
. Inside the passed in continuation, we can usebreak'
to perform branching. TheLabel
type 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".