/wasm-hs

Type-safe WASM eDSL in Haskell

Primary LanguageHaskellGNU General Public License v3.0GPL-3.0

wasm-hs

wasm-hs Logo

This project provides an embedded domain-specific language (eDSL) that enables the embedding of a significant subset of WebAssembly into Haskell, in a type-safe way (i.e. writing invalid Wasm results in a Haskell type error).

Below is a simple example of the DSL, a Wasm program that counts up to 10:

countTo10 = main do
  const @Int 0
  let' #i do
    loop #next do
      local.get #i
      const 1
      add
      local.tee #i
      dup
      print
      const 10
      cmp.lt
      br_if #next

Supported WebAssembly features

  • Arithmetic and comparison instructions
  • Blocks and structured control-flow
  • Local and global variables
  • Type-safe dynamic memory access
  • Recursive functions

Ergonomics

In order to provide better ergonomics as a Haskell DSL, the project deviates from the WebAssembly specification in a few aspects:

  • The operand stack can contain values of any Haskell type, not just the Wasm primitive types (i32, i64, f32, f64).
  • Arithmetic and comparison instructions are overloaded using the standard Haskell typeclasses (Num, Ord etc.).
  • Boolean instructions (comparisons, br_if etc.) use Haskell's native Bool type, rather than encoding booleans as 0 or 1 of type i32.
  • Variables use alphanumeric names (rather than numeric indices), and are scoped explicitly (using a let' instruction), rather than being function-scoped.
  • Similarly, block labels are also named, and are introduced by the block instructions (block, loop, if).
  • Rather than using a single untyped linear memory, the user can explicitly allocate typed memory segments, inspired by the MSWasm paper. Segments are scoped similarly to local variables, and are allocated with the let_seg instruction.

Interpretation

The project also includes an interpreter that uses continuation-passing style for efficient jumps, and local instances (via WithDict) for constant-time variable lookup.

Global variables and segments can be initialised with host-provided mutable references (IORefs), which allows the host to pass inputs to the Wasm module, and inspect its outputs and side-effects.

Limitations

  • The DSL only allows the construction of self-contained Wasm modules (i.e. no external imports or exports).
  • Functions can only refer to functions defined before them in the same module (and to themselves), thus mutually-recursive functions are not supported.
  • Indirect calls and br_table are not supported.

Project Structure

The main modules of the library are Language.Wasm.Instr, which defines the core Instr AST datatype and evaluation functions; and Language.Wasm.Module, which builds upon Instr and defines a datatype for bundling definitions into modules, as well as module evaluation functions.

Language.Wasm.Syntax defines the DSL's syntactic sugar, and Language.Wasm.Prelude ties everything together into a single import.

The Examples module defines a number of example Wasm programs, and Main contains the driver code.

Dependencies

  • GHC >=9.8.1
  • Cabal >=3.10.2.0

(Both can be installed via GHCup)

Building and running

The project has no external (non-Haskell) dependencies, and can simply be built using cabal:

cabal build

Running the project with no arguments will run all the example programs:

cabal run

If you want to run only specific examples, pass their names as arguments:

cabal run . -- factorial fibonacci

If you specify an example multiple times, it will be executed that many times. This can be used to observe side-effects such as mutating memory shared by the host:

cabal run . -- squareAll squareAll

Running the tests

To run the library's test-suite, use the following command:

cabal run wasm-hs-test