Learning WebAssembly #3: Wat Programming Basics

Learning basic building blocks of programming in Wat.


In the first part of this series, we have seen a little Hello world program written in Wat. Albeit simple enough, it can be even simpler.

To compile Wat code into binary Wasm, you can use the WebAssembly Binary Toolkit: wat2wasm test.wat -o test.wasm

The following code is literally the simplest valid Wat program:

(module)

Nice, but not particularly useful. It does show us, however, one important concept of programming in Wat: S-expression.

S-expressions

S-expressions (aka symbolic expressions) are a simple textual notation for representing trees. Basically, it is about nesting parenthesized lists. If you have ever seen the syntax of Lisp you already know S-expressions.

The module above is an example of a simple tree written as an S-expression. Well, it is a bit degenerated tree; it is only a standalone root. A slightly better example follows:

(module (memory 1) (func))

Here, we have a simple tree with one root module and two child nodes memory and func. The node memory has one attribute 1.

Every Wat program is practically a big S-expression with the root module.

Stack Machine

Wasm execution is defined in terms of a stack machine. Each instruction pushes and/or pops a certain value to/from a stack.

We can demonstrate this idea with our favourite example:

(module
  (func (export "main") 
        (result i32)
    i32.const 42 
    return))

The code above defines a single function that is exported under the name main. The export name is fully arbitrary, it would work as hello or Answer_to_the_Ultimate_Question as well. The result type is of 32-bit integer.

The body of the function has exactly two instructions: i32.const and return. The former pushes the literal 42 to the stack; the latter pops the value from the stack and returns as the function result.

i32.const x
Pushes the 32-bit value x to the stack.
return
Exits the current function and returns the value(s) on the top of the stack.

We can achieve the same result by nesting S-expressions:

(module
  (func (export "main") 
        (result i32)
    (return 
      (i32.const 42))))

The stack-based programs are, however, much closer to how the program really executes, so, further on, we will stick with the stack-based programming model.

BTW, the return instruction could be omitted as the function returns the value(s) on the top of the stack automatically.

A more advanced example would be a function for adding two numbers:

(module
  (func (export "sum") 
        (param $a i32)
        (param $b i32)
        (result i32)
    local.get $a
    local.get $b
    i32.add
    return))

Here, the exported function sum takes two parameters named $a and $b and returns a 32-bit integer result. Parameters are practically local variables with values initialized by the caller.

The instruction local.get pushes a variable to the stack, the instruction i32.add adds the two topmost values and pushes the result to the stack.

local.get $x
Pushes the value of the variable $x to the stack.
i32.add
Pops the two topmost values on the stack, adds them, and pushes the result to the stack.

Enough the theory. Now, we will finally jump into programming.

Control Flow

Let’s take a look at control instructions in Wat: conditions and loops.

We will demonstrate these with a basic factorial algorithm. First, we must recall the algorithm:

function factorial(n) {
  if (n <= 2) return n;
  let fact = 1;
  for (let i = 2; i <= n; i++) {
      fact = fact * i;
  }
  return fact;
}

Now, we program the same algorithm as a Wat function. The signature is straight-forward; one parameter, two local variables:

(func (export "fac_loop")
      (param $n i32)
      (result i32)
      (local $i i32)
      (local $fac i32)
  ...)

For the first condition, we use if-else-end control instructions:

(func (export "fac_loop")
      (param $n i32)
      (result i32)
      (local $i i32)
      (local $fac i32)
  i32.const 2
  local.get $n
  i32.ge_s
  if (result i32)
    local.get $n
    return
  else
    ...
  end)
i32.ge_s
Pops the two topmost values on the stack as signed 32-bit integers, compares them to be greater or equals, and pushes the result to the stack.

We simply push $n and 2 values to the stack and check them on being greater or equal. The factorial calculation happens in the else block:

(func (export "fac_loop")
      (param $n i32)
      (result i32)
      (local $i i32)
      (local $fac i32)
  i32.const 2
  local.get $n
  i32.ge_s
  if (result i32)
    local.get $n
    return
  else
    i32.const 1
    local.set $fac
    i32.const 2
    local.set $i

    loop
      local.get $i
      local.get $fac
      i32.mul
      local.set $fac

      local.get $i
      i32.const 1
      i32.add
      local.set $i
      
      local.get $n
      local.get $i
      i32.ge_s
      br_if 0
    end

    local.get $fac
    return
  end)

The loop instruction is controlled by the br_if instruction. If the condition passes, the loop continues.

local.set $x
Pops the value on the top of the stack and assigns it to the variable $x.
i32.mul
Pops the two topmost values on the stack, multiplies them, and pushes the result to the stack.

The parameter of br_if is an index of the loop. Alternatively, an explicit label can be used:

loop $myloop
  ...
  br_if $myloop
end

Calling Functions

Decomposition is a handy tool every programmer should employ. Using the recursive factorial algorithm as an example we will show how to call functions inside a function in Wat.

The algorithm reads as follows:

function factorial_rec(n) {
  if (n <= 2) return n;
  return n * factorial_rec(n - 1);
}

The signature and first condition should be no surprise:

(func $fac_rec
      (export "fac_rec")
      (param $n i32)
      (result i32)
  i32.const 2
  local.get $n
  i32.ge_s
  if (result i32)
    local.get $n     
  else
    ...
  end
  return)

An important difference is the function name $fac_rec. This makes it easier (in comparison to using indexes) to call the function within the module:

(func $fac_rec
      (export "fac_rec")
      (param $n i32)
      (result i32)
  i32.const 2
  local.get $n
  i32.ge_s
  if (result i32)
    local.get $n     
  else
    local.get $n
    i32.const 1
    i32.sub
    call $fac_rec
    local.get $n
    i32.mul
  end
  return)

The instruction call calls the referenced function with the parameter from the stack. The result of the call is pushed to the stack and multiplied by the value of the variable $n.

Further Steps

This time, we have focused on writing functions in Wat. We have shown function signatures, variables, and control flow instructions.

In the next part of this series, we will explore the memory concept of Wasm and how to use it for working with complex data types such as strings.

Stay tuned!