This post talks about writing WebAssembly by hand (using its textual format), and mentions a new GitHub repository I've created with code samples.

A bit of nomenclature first. WASM stands for WebAssembly - it has a binary format and a textual format. The textual format, called WebAssembly Text or WAT, is the subject of this post.

Introduction to WAT

WASM is a stack machine, and while stack machines can lead to wonderfully compact bytecode, they can also be awkward to code by hand - because the programmer needs to have a mental model of the top stack slots at all times, remembering what they refer to. While you can certainly code directly to the stack machine with WAT, it also has some programmer-friendly constructs that significantly improve writability and readability. Here's an example:

(local.set $writeidx (i32.sub (local.get $writeidx) (i32.const 1)))

This is equivalent to writeidx -= 1 in many mainstream languages. The two WAT features at play here are:

  1. The ability to declare variables and to refer to them by name (this includes function parameters).
  2. Folded instructions - allowing the programmer to condense a sequence of stack operations into a single s-expr. This is music to my Lisper ears!

These folded instructions can go as deep as we wish; here's an even more nested example involving memory access:

(local.set
    $next_env_ptr
    (i32.load (i32.add  (global.get $env_ptrs)
                        (i32.mul (local.get $i) (i32.const 4)))))

In pseudo-C, this is equivalent to [1]:

next_env_ptr = memory[env_ptrs + i*4];

WAT has some additional ergonomic features that I like. For example, named functions with named parameters, as well as declared return values:

(func $itoa (export "itoa") (param $num i32) (result i32 i32)
  ...
)

This function has a name we can refer to in calls, a single parameter with a name ($num) and two return values. Calling this function can be done in a folded expression like:

(call $itoa (i32.add (local.get $n) (i32.const 1)))

Which is equivalent to itoa(n+1). Another feature this example demonstrates is types - WAT functions and values (parameters, globals and locals) have types, which makes code easier to read and understand, and also provides the compiler an opportunity to check for correctness at compile time.

Moreover, in the WASM model, type checking goes deeper and extends to stack interactions; the WASM compiler knows how many stack slots each instruction uses and produces, and this is verified as well - so common mistakes are easily caught. I find that the code is much more often correct once I get it to compile in WAT compared to other assembly languages.

Samples of WAT code

Back to the original goal of this post. While I enjoy writing WAT code, one aspect of the experience that could be improved is documentation. The WASM spec is much more suitable for formal verification than for actual documentation purposes; specifically, it's hard to grep and doesn't provide much in terms of examples. This is alright for a spec, but I couldn't find complementary resources that just show code samples.

Therefore, I've decided to collect some of the WAT snippets I've written so far into a GitHub repository named wasm-wat-samples. It's my humble contribution to the world of WAT documentation. The goal of the repository is to demonstrate how WAT concepts (including WASI) and constructs are used in practice; it's optimized for greppability.

I hope others find it useful as well - feel free to suggest additional samples in issues and PRs!


[1]memory is implicitly the linear heap memory every WASM module has.