Before we can get started writing WAT code, there are some important parts you need to understand. The syntax used is not like most other programming languages, there are some unusual limitations, and you need to think a little differently when writing your code.
These basic parts are the bedrock of what makes WAT work and it is something you will need to understand before you can move on to other sections.
In other programming languages there are a wide range of types to choose from. Integers, floating point numbers, strings, date classes, and others. But in WAT you only have a limited number you can use.
i32 | 32 bit unsigned integer 0 to 2^32 |
i64 | 64 bit unsigned integer 0 to 2^64 |
f32 | 32 bit floating point number |
f64 | 64 bit floating point number |
v128 |
128 bit vector containing either, 2x 64 bit floats 4x 32 bit floats 2x 64 bit unsigned integer 4x 32 bit unsigned integer 8x 16 bit unsigned integer 16x 8 bit unsigned integer |
funcref | Reference to a function |
The integers are unsigned, which means they do not have a negative bit, they store all the bits as they are. However there are instructions in WAT that treat the integers as being either signed or unsigned, so you can use negative numbers.
You use the i32 type as a Boolean, with the value 0 meaning FALSE
and any other value meaning TRUE
.
There is no string type, which can seem odd, but there is a memory area we can create and use to hold text data in whatever format we need.
This does all seem very limiting but it is how things look at a machine code level. These are the building blocks you use to make more complex structures, and it is how data is seen and used at the lowest of levels.
At a machine code level, to use instructions like ADD
, you would either use a CPU register or a memory location.
This is just some place to put the parameters before calling the instruction and somewhere to store the result.
Because Web Assembly is machine independent, and each machine can have different CPUs and different registers,
an alternative method was needed to do the same sort of thing.
A stack machine is used to temporarily store parameters and results for instructions and functions in an easy to understand way. It may not be the way the data is handled on the running computer, after it has loaded in the WASM file, but from a WAT point of view it is a simple list of data that you can put data on and take off. It is very different to anything you may have used before, but it is easy to use once you understand what is going on.
To visualise this, imagine a list of items starting at the bottom and going up.
When you want to add something to the stack you PUSH
it on to the top.
When you want to take something off, you POP
it off from the top.
It is a Last In, First Out (LIFO) principle.
12 | Top of Stack |
502 | |
332 | Bottom of Stack |
PUSH
the value 9123 onto the stack.
>> 9123 | Top of Stack |
12 | |
502 | |
332 | Bottom of Stack |
POP
the top most value off the stack.
<< 9123 |
12 | Top of Stack |
502 | |
332 | Bottom of Stack |
You can add any data type to the stack, however they need to match the requirements of the instruction or function you want to call.
For example, i32.add
requires two i32
values on the stack, so using i64
will create a problem.
The best way to understand this is by showing an example. Let's say we want to perform the following calculation in WAT.
= 100 - 42
To perform this operation we need to use the i32.sub
instruction.
This requires two values on the stack.
This is done in the following way.
;; Push 101 onto the stack i32.const 101 ;; Push 42 onto the stack i32.const 42 ;; Subtract 42 from 101 i32.sub
At the start the stack is empty. We PUSH
the value 101 and 42 onto the stack.
42 | Top of Stack |
101 | Bottom of Stack |
The i32.sub
instruction will POP
the last two stack items off the stack, subtract 42 from 101,
and then PUSH
the result back onto the stack at the top.
59 | Top of Stack |
The order you PUSH
items onto the stack is important.
If you had changed the order around in the example above then you would have performed 42 - 101 instead.
The information in a WASM file is in a tree-like format, with a parent object containing sub child objects, and with them also containing sub child objects. This is shown in WAT using symbolic expressions (S-Expressions for short). It looks something like this.
(parent (sub-child-1 (sub-sub-child-1.1) (sub-sub-child-1.2) (sub-sub-child-1.3) ) (sub-child-2) )
You surround the object with parentheses (...)
.
There needs to be a label, instruction, keyword, or something after the first (
character before you include other objects.
This is what the tree-like structure for the above example looks like.
We can write the same set of operations in a number of different ways in WAT. The first example shows it written without any S-Expressions.
local.get $first local.get $second i32.sub
We get the $first and $second parameters and push them onto the stack. We subtract the $second value from the $first and push the result onto the stack. For the next example we will do the same thing but on one line using an S-Expression.
(i32.sub (local.get $first) (local.get $second))
It looks backwards.
It seems that we are calling i32.sub
before putting the parameters onto the stack.
We can open this up a little to see how else it can be written.
;; Open it up #1 (i32.sub (local.get $first) (local.get $second) ) ;; Open it up #2 ( i32.sub ( local.get $first ) ( local.get $second ) )
To understand what is happening here we need to look at it in a tree-like way. Take a look at the following diagram.
The parent node is the i32.sub
instruction, which has the two sub-child objects, the first being on the left and the second on the right.
We are in a way saying that the parent object needs to be processed, and to do so we need to process its sub-children object first, and in the given order.
All these different ways of writing the operation doesn't really matter. The WASM code that is created looks exactly the same. You can write it in whichever way seems best for you.
You will need to add comments to your WAT programming, as it can be difficult to know what you are trying to do just by looking at the code. There are two ways to add a comment.
;; This is a one line comment
The comment starts with two semicolons ;;
and ends when the line breaks.
Ideal for a small comment.
(; This
is
multilined
comment
;)
This comment starts with a parentheses and semicolon (;
and can continue for many lines until it finds the ending ;)
characters.
This is good for a large body of text with detailed comments.
All comments do not end up in the final WASM file.
When naming parameters, functions, local variables, loops, and other things within WAT, you have to start them with a $
character.
After that it can only contain the following ASCII characters (and no spaces).
0
to 9
A
to Z
a
to z
and any of the following,
!
#
$
%
&
'
*
+
-
/
:
<
=
>
?
@
\
^
_
`
|
~
(func $add (param $first i32) (param $second i32) (result i32)...)
Here the function and parameter names all start with the $
character.