Learning WebAssembly #3: Wat Programming Basics
Learning basic building blocks of programming in WebAssembly text format (Wat).
In the first part of this series, we saw a little Hello world program written in Wat. Albeit it was 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!