You can control the path a program takes using conditions and the if
then
else
end
keywords.
This section will help you understand how to use them within WAT and what instructions are available.
Controlling the path a program takes depending on different conditions is a fundamental part of designing an application and it is important
to learn how to use the instructions WAT contains.
In your program you will want to do something only if a condition is met, if age is over 65 then add a discount, for example.
Let's look at how the if
keyword works.
;; Push 1 onto stack i32.const 1 if ;; Because the stack had a non-zero value ;; this part will be processed. end ;; Push 0 onto stack i32.const 0 if ;; Because the stack had a zero value this ;; part will NOT be processed. end
The if
instruction will pop the last item off the stack and see if it is 0 or not.
Therefore, before the if
instruction is used, you need to make sure you have added the condition value onto the stack.
This condition value is how the if
statements work and how they are controlled.
If the condition value is 0 then the code inside the if
block is not executed.
If the condition value is something other than 0 then it will continue to execute whatever is inside the block.
The condition value can only be an i32
data type.
We will look into condition instructions used for different data types later.
But, the if
instruction can only work when an i32
value is on the stack before it is called.
In the example above, in the first part, we are pushing the conditional value 1 onto the stack.
Next the if
instruction will pop that value off the stack, see that it is not zero, and then move the execution inside the block.
In the second part of the example, we push the condition value 0 onto the stack.
This time the if
instruction, after popping it off the stack, will see that it is zero, therefore the condition has not been met and
the execution is moved past it, not touching the block of code inside.
But what happens if the condition is not met, can we do something else?
This type of “if condition met then do A otherwise do B” can be done using the else
keyword.
;; Push 1 onto stack i32.const 1 if ;; Because the stack had a non-zero value ;; this part will be processed. else ;; But, if it was a zero then this part ;; would have been processed. end
In this example we are using the else
keyword to split the if...end
block in two.
If the condition value is non-zero then the execution path is moved to the top part, but if the condition value is
zero (the condition was not met) then the execution path moves to the bottom block, skipping the top block.
Either the top section is executed or the bottom section.
In the example above, because we are pushing the condition value 1 onto the stack, the condition is met and therefore the top section is processed. If we change the code so that we push the value 0 instead, then the condition would not be met, and therefore the bottom section would be executed.
The if
instruction only needs an i32
value on the stack for it to work.
This can get there in a number of different ways.
There are different condition instructions that allow you to compare two numbers.
We can see if one number is equal to another number for example.
;; Push i32 value 42 onto the stack i32.const 42 ;; Push i32 value 101 onto the stack i32.const 101 ;; Compare the first number with the second one (42 == 101) i32.eq ;; If 42 is equal to 101 if ;; Yes 42 is equal to 101 else ;; No 42 is not equal to 101 end
In this example we are pushing two i32
values onto the stack and calling the i32.eq
instruction.
This “equal” instruction pops the last two values off the stack and compares them, seeing if they are equal or not,
and then pushes the result onto the stack.
In this case, 42 does not equal 101, and therefore the i32
value 0 is pushed onto the stack.
This then allows the if
instruction to process the result of the condition and execute the code required, which in this case is the bottom block,
the 42 is not equal to 101 section.
;; Push f64 value 3.142 onto the stack f64.const 3.142 ;; Push f64 value 2.718 onto the stack f64.const 2.718 ;; Compare the first number with the second one (3.142 == 2.718) f64.eq ;; If first number was equal to second if ;; Yes first number was equal to second else ;; No first number was not equal to second end
This time we are comparing f64
numbers and using its f64.eq
instruction.
This pops the two f64
values off the stack and compares them, checking if they are both the same, and then pushes the result,
an i32
value, onto the stack.
The result is not of the same type, it is not a f64
value, but the same i32
data type as before.
All comparison instructions return an i32
data type value.
This tests if a single value on the stack is zero or not.
It is only available to the i32
and i64
data types, there aren't any for floating point numbers.
To perform this you use the i32.eqz
instruction.
It is only looking at one number, so only one item needs to be on the stack before calling the compare instruction.
;; Add i32 value 0 onto the stack i32.const 0 ;; Compare to see if it is zero i32.eqz ;; The result on the stack is an i32 value of 1
The last value on the stack is 0, which is zero and therefore the compare instruction will push the result 1 onto the stack.
This compares two numbers and sees if they are the same value.
It is available for all data types.
To check both numbers are equal you use the i32.eq
instruction (or whichever data types you are comparing).
This needs two values, of the same data type, pushed onto the stack before you can call the instruction.
;; Add f64 value 3.142 onto the stack f64.const 3.142 ;; Add another f64 value 3.142 onto the stack f64.const 3.142 ;; Compare to see if both are the same f64.eq ;; The result on the stack is an i32 value of 1
Two f64
values are pushed onto the stack, both of which have the same value.
The order you push the values onto the stack does not matter, the result will be the same.
Then we call the f64.eq
instruction, which pops the two values off the stack, compares them to see if they are equal,
and pushes the result onto the stack.
In the example above, the result is an i32
data type with the value 1, telling us that both the f64
values are the same.
With this instruction we can compare two numbers and check that they do not have the same value.
This can be used with all data types.
To perform the check you use the i32.ne
instruction.
This will need two values pushed onto the stack.
The data type needs to be the same as the calling instruction before we call it.
;; Add f64 value 3.142 onto the stack f64.const 3.142 ;; Add another f64 value 2.718 onto the stack f64.const 2.718 ;; Compare to see if both are not the same f64.ne ;; The result on the stack is an i32 value of 1
We push two f64
values onto the stack, each containing a different value.
The values can be pushed onto the stack in any order, it will not change the result.
Afterwards the f64.ne
instruction is called, which pops off the two values from the stack, compares to see if they are different to one another,
and then pushes the result value onto the stack.
The result in the example above is an i32
value of 1, stating that both numbers are not the same.
Comparing if one number is greater or lesser (or equal) than another is done differently between integers and floating point numbers.
The f32
and f64
data types are natural numbers and can be positive or negative.
But the i32
and i64
data types can be seen as signed (can be negative) or unsigned (positive only) and
therefore there are different versions for each comparison instruction.
First let's look at the floating point number comparison.
Instruction | Details |
---|---|
gt | Greater Than |
ge | Greater Than or Equal To |
lt | Less Than |
le | Less Than or Equal To |
;; Add f64 value 3.142 onto the stack f64.const 3.142 ;; Add another f64 value 2.718 onto the stack f64.const 2.718 ;; Compare to see the first > second value f64.gt ;; The result on the stack is an i32 value of 1
The first number pushed onto the stack is checked against the second number.
The order this is done is important and will affect the result.
You are checking if the first value is greater than the second.
The f64.gt
instruction pops the two values off the stack, compares them, seeing if the first is greater than the second, and pushing the result onto the stack.
Now when it comes to integers we have signed and unsigned versions.
Instruction | Details |
---|---|
gt_s | Greater Than (signed) |
gt_u | Greater Than (unsigned) |
ge_s | Greater Than or Equal To (signed) |
ge_u | Greater Than or Equal To (unsigned) |
lt_s | Less Than (signed) |
lt_u | Less Than (unsigned) |
le_s | Less Than or Equal To (signed) |
le_u | Less Than or Equal To (unsigned) |
;; Compare the numbers 0xFE000001 and 0x000000AB (signed) i32.const 0xFE000001 i32.const 0x000000AB i32.gt_s ;; Result 0xFE000001 (-33554431) > 0x000000AB (171) is 0 ;; Compare the numbers 0xFE000001 and 0x000000AB (unsigned) i32.const 0xFE000001 i32.const 0x000000AB i32.gt_u ;; Result 0xFE000001 (4261412865) > 0x000000AB (171) is 1
Here we are comparing two numbers twice. The first time we are looking at the number as being signed (can be negative), and then a second time with them being unsigned (positive only). They are the same two numbers, but with the first number, the first time it looks negative and the second it looks positive.
The first part we are using the i32.gt_s
instruction, which is the signed version, and looks at the values 0xFE000001 and 0x00000AB as -33554431 and 171.
Because the first number is smaller than the second the result will be 0, it is not greater than.
In the second part we use the i32.gt_u
instruction, the unsigned version, which looks at the values 0xFE000001 and 0x00000AB as 4261412865 and 171.
This time the first number is greater than the second and therefore sets the result as 1.
You need to be careful how you see the integers you are using, signed or unsigned, and which comparison functions you are using. Get it wrong and the results will not be what you expect.
So far we have looked at making a single condition but how would we handle two or more conditions at the same time. For example, if the age is over 18 and under 65, how do we do this in WAT?
;; Compare $age is over 18 local.get $age i32.const 18 i32.gt_s ;; If $age > 18 if ;; Compare $age is under 65 local.get $age i32.const 65 i32.lt_s ;; If $age < 65 if ;; Age is within range... end end
In this example we have one condition inside another.
We first check if the $age
variable is greater or equal to 18.
If it is then move inside the if
block and then we perform the second condition, which is checking if
the $age
variable is less than or equal to 65.
Here we have nested the second condition check inside the first.
;; Compare $age is over 18 local.get $age i32.const 18 i32.gt_s ;; Compare $age is under 65 local.get $age i32.const 65 i32.lt_s ;; Perform bitwise AND on both compare results i32.and ;; If $age > 18 AND $age < 65 if ;; Age is within range... end
Another way of doing this is to perform the conditions one after the other and then use a logical AND operation on the results. We need to walk through the above example to understand what is happening.
$age
to the value 18.
We want to know if it is greater than or equal to it.
$age
again but this time to 65.
We want to know if it is less than or equal to it.if
instruction.
It will function just like before, moving the execution inside if the stack contains a non-zero value.
You can chain condition functions and logical operations together to create more complex condition statements. Choosing which approach to use will depend on what your needs are and what seems best at the time.
So far we have used a flat style of formatting, but with S-Expressions you can write them differently, in a more compact way.
This can be useful if you have a lot of if
statements close together.
;; Push 1 onto stack i32.const 1 ;; If statement with S-Expression (if (then ;; Process if true ) (else ;; Process of false ) )
The if
statement is being used with an S-Expression starting and ending bracket.
Also the then
and else
parts have surrounding brackets.
The condition is being set outside, but it can be performed inside.
;; If statement with S-Expression (if ( ;; Push 1 onto stack i32.const 1 ) (then ;; Process if true ) (else ;; Process of false ) )
The condition part has been moved inside the if
S-Expressing.
It is not labelled, so it must be the first block after the if
statement.
We can now be able to put everything onto a single line.
;; Check the $n value and set $result (if (i32.eq (local.get $n)(i32.const 1)) (then (f64.const 1.0) (local.set $result))) (if (i32.eq (local.get $n)(i32.const 2)) (then (f64.const 2.0) (local.set $result))) (if (i32.eq (local.get $n)(i32.const 3)) (then (f64.const 6.0) (local.set $result)))
Everything we want to do, for each condition, now easily fits onto the one line. Our code would start to look bulky if we did the same thing using the previous approach.
Inside the if
statement block we may want to use something that was placed on the stack before, or
we may want to add something to the stack that will be used after.
Let us look at both with an example.
;; Push i32 value 10 onto the stack i32.const 10 ;; Perform condition i32.const 1 if ;; Add 2 to the 10 value already on the stack i32.const 10 i32.add end
In this example we want to add 10 to whatever value was pushed onto the stack before the if
statement.
Seems perfectly normal, but it will not compile, the wat2wasm
tool will output an error message.
;; Perform condition i32.const 1 if ;; Push 20 onto the stack i32.const 20 end
Here we want to push the value 20
onto the stack if the condition is met.
But what if the condition is not met, nothing will be put on the stack.
You will also get an error message when compiling this.
The result for these issues is because we need to see the if
statement as its own block, which can have parameter inputs and result outputs.
These are just like function's parameter list and result statements.
First we need to look at the result part.
;; Perform condition i32.const 1 if (result i32) ;; Push 10 to stack i32.const 10 else ;; Push 20 to stack i32.const 20 end
The if
statement block has a new part added, which is saying that the block will add a single i32
value onto the stack when it has finished.
If there was no else
section then it would not compile and would give an error message.
This is because both the true and false parts of the if
block need to add a single i32
value onto the stack.
If the condition is not met then nothing would be added to the stack which would be unbalanced.
;; Add value to stack i32.const 42 ;; Perform condition i32.const 1 if (param i32) (result i32) ;; Add 10 to parameter and push result onto stack i32.const 10 i32.add else ;; Add 20 to parameter and push result onto stack i32.const 20 i32.add end
This adds either 10 or 20 to the parameter placed on the stack before the if
statement is processed.
The block has had a list of parameters added to it.
This states that the block is expecting a single i32
value on the stack.
You need to make the if
statement balanced when you are pushing and popping items on and off the stack, so
that the true part of the block matches the false part.
If they do not then the compiler will output an error message about it.