On this page

Numbers

Manipulating numbers is the most common part of any algorithm and is something you use throughout any application. This section explores how numbers work in WAT and how to use the instructions available to control and manipulate them in the many different ways you need. It is important to understand what a number is, what it can do, and what it cannot do.

Integers

An integer is used to store whole numbers only. They are the primary data type used in computing. In WAT there are two integer types, i32 and i64. They work in the same sort of way but have different sizes. You properly know how integers work, but here is a quick look.

The i32 integer type is made up of 32 bits, which can be either 0 or 1, with the least significant bit on the right and the most significant bit on the left. For example:

Bit: 31 30 29 28 27 26 25 24 ... 7 6 5 4 3 2 1 0

We can write the number in binary (with only 0s and 1s) like this.

01001011 = 75 decimal

You may also see the same number written as hexadecimal (with 0 to 9, then A to F) like this.

0x4B = 75 decimal

The 32 bits is just data and on its own only represents a number. It can be used in many different ways to represent different things. For example, the value 75 decimal is the ASCII character K.

To use negative values the integer will need to be handled in a different way. Normally we use all 32 bits of the number, but if we want to use negatives, then we use the 31st bit on the left, the most significant bit, as a special flag, to tell the different instructions that the number is negative and not positive. For example,

0xFFFFFFFF = 4294967295 decimal (using all 32 bits for the whole number)
0xFFFFFFFF = -1 decimal (bit 31 is a flag making it a negative number)

It is the same data but we are looking at it in two different ways. When the negative flag is not being used we call the integer unsigned, but when it is being used we call it signed. We will see a number of instructions that have both an unsigned and a signed version.

Most of the time you will be working with the i32 data type instead of the i64 data type. Also, in JavaScript numbers are only converted into 32 bit integers. You would need to use the BigInt object to work with i64 values.

Floats

To work with real numbers, those that have decimal points, we cannot use normal integers, but instead we need to use a floating point type. In WAT there are two available, f32 and f64, both of which are basically the same but have different sizes, with the 64 bit version having a higher precision.

The f32 type is using the same 32 bits as an i32 integer but it is being used to represent a floating point number instead. This includes negative numbers too.

It is better to use the f64 type when working with floating point numbers, as the f32 data type is older and has lower precision. Also, JavaScript numbers are using the 64 bit floating point data type too, so transferring between the two is more simple.

Adding

To add two i32 numbers together you use the i32.add instruction. You can also use the i64.add, f32.add and the f64.add instructions for different data types. You cannot add one data type to a different one however.

;; Push the i32 value 42 onto the stack
i32.const 42

;; Push the i32 value 101 onto the stack
i32.const 101

;; Pop the last two i32 values off the stack, add
;; them both together and push the result onto
;; the stack
i32.add

;; The stack contains an i32 with the value 143

As you can see, adding two numbers together requires more work than most other programming languages. We are using the stack to hold the parameters that the i32.add instruction will use. In the example above we have pushed two constants onto the stack (42 and 101). The instruction pops those i32 values off the stack, adds them together, and then pushes the result onto the stack.

The order you push the values onto the stack does not matter, the result will always be the same. However, with integer numbers you need to be aware of their limits.

;; Push the two i32 values onto the stack
i32.const 4294967295
i32.const 2

;; Add them together
i32.add

;; The stack contains an i32 with the value 1

Because the first number is very close to the limit, and adding more to it so that it goes over the limit, makes the integer value overflow back around and restart at zero again. The end result is not mathematically correct.

Multiplying

To multiply two i32 numbers together you use the i32.mul instruction. There is an instruction to do the same thing for the different data types, i64.mul, f32.mul and f64.mul.

;; Push the i32 value 42 onto the stack
i32.const 42

;; Push the i32 value 3 onto the stack
i32.const 3

;; Pop the last two i32 values off the stack, multiple
;; them both together and push the result onto
;; the stack
i32.mul

;; The stack contains an i32 with the value 126  

The process is similar to adding two numbers together, requiring you to use the stack to hold the two parameters before calling the i32.mul instruction. The above example shows two constants being pushed onto the stack (42 and 3). These are popped off the stack, multiplied together, and the result is pushed onto the stack.

You can place the values onto the stack in any order. The result will be the same no matter which way round it is done. Again, when using integers, you need to know about their limits.

;; Push the two i32 values onto the stack
i32.const 3724541951
i32.const 12345

;; Multiple them together
i32.add

;; The stack contains an i32 with the value 1845481415
;; instead of 45979470385095  

When you multiply both numbers together the end result is larger than the maximum value of a 32 bit integer value. The result is a number that gets truncated into the wrong value (mathematically speaking).

Subtraction

To subtract one number from another you use the i32.sub instruction. You can also use the i64.sub, f32.sub and the f64.sub instructions for the different data types.

;; Push the i32 value 101 onto the stack
i32.const 101

;; Push the i32 value 42 onto the stack
i32.const 42

;; Pop the last two i32 values off the stack, subtract the
;; top most from the previous one and push the result onto
;; the stack, result = 101 - 42
i32.sub

;; The stack contains an i32 with the value 59  

The instruction needs the two numbers to be pushed onto the stack, keeping in mind that the order you do this is important. The second number (42) is being subtracted from the first number (101). Doing this the other way round would give a different result.

101 - 42 = 59 decimal = 0x3B hexadecimal
42 - 101 = -59 decimal = FFFFFFC5 hexadecimal  

With integers the numbers are treated like signed numbers. So negative amounts can be used.

Division

When using floating point numbers, f32 and f64, you can divide one number by another using the f32.div and f64.div instructions. Things are a little different when using integers and we will get back to that a little later.

;; Push the f64 value 3.142 onto the stack
f64.const 3.142

;; Push the f64 value 2 onto the stack
f64.const 2

;; Pop the last two f64 values off the stack, divide the
;; top most by the previous one and push the result onto
;; the stack, result = 3.142 / 2
f64.div

;; The stack contains a f64 with the value 1.571

To perform the instruction you need to first push the two values onto the stack. However, you need to be careful what order you do it in. You are dividing the first number (3.142) by the second number (2). Doing this the other way round would give a different result.

3.142 / 2 = 1.571 decimal
2 / 3.142 = 0.636537 decimal    

Dividing integers is done a little differently. An integer can be seen as signed (can be negative) or unsigned (positive only). As a result there are two instructions you can use, i32.div_s (for signed) and i32.div_u (for unsigned).

;; Push the i32 value onto the stack
i32.const 0xF178CD56
i32.const 2

;; Divide 0xF178CD56 by 2 seeing it as signed
;; result = -243741354 / 2 = -121870677
i32.div_s

;; Push the i32 value onto the stack
i32.const 0xF178CD56
i32.const 2

;; Divide 0xF178CD56 by 2 seeing it as unsigned
;; result = 4051225942 / 2 = 2025612971
i32.div_u

In the above example we are dividing the same number by 2, but in the first case we are looking at the value as being signed, as a negative number. In the second part we are looking at the number as unsigned, as positive only. The end results are both different. You need to be careful how you are treating the integers being used, either as signed or unsigned.

Remainder

When you divide one integer by another you may have some amount left over. This is the remaining amount, the remainder (or modulus), for example, 10 / 3 is 3, thats (3 * 3) = 9 with 1 remaining. To calculate the remaining amount you use the i32.rem_s and i32.rem_u instructions. There are some for the i64 data type, but not for f32 or f64.

;; Push the i32 value 10 onto the stack
i32.const 10

;; Push the i32 value 3 onto the stack
i32.const 3

;; Pop the last two i32 values off the stack, find the remainder
;; of dividing the top most by the previous one and push the result
;; on to the stack, result = 10 % 3
i32.rem_s

;; The stack contains an i32 with the value 1

The instruction needs to be performed by pushing the two values onto the stack. Take care though, you need to make sure the order is correct. You are dividing the first number (10) by the second number (3) and getting the remainder. You will get different results if you do it the other way round.

10 % 3 = 1
3 % 10 = 3    

The above example used the signed instruction. This means it works differently with negative numbers. There is an unsigned instruction that treats the 32 bit number as a positive number only. You need to make sure you are using the right one otherwise you could get a result you didn't want.

;; Push the i32 values onto the stack
i32.const 0xF178CD56
i32.const 42

;; Get remainder from 0xF178CD56 by 42 seeing it as signed
;; result = -243741354 % 42 = -24
i32.rem_s

;; Push the i32 values onto the stack
i32.const 0xF178CD56
i32.const 42

;; Get remainder from 0xF178CD56 by 42 seeing it as unsigned
;; result = 4051225942 % 42 = 22
i32.rem_u  

In the example above we are looking for the remainder of dividing the same number by 42, with the first case looking at it as a signed number (a negative value). For the second case the number is being looked at as unsigned (positive only). You end up with two different results. Take care how you look at the integers, seeing them as either signed or unsigned.

Math

The f32 and f64 data types have a number of other math related instructions to help you manipulate floating point numbers.

Absolute Converts a number into a positive only one. If the value is negative then it gets negated into a positive one. If the value is already positive then nothing is changed.
Negate Swaps the sign of the number. If the value is negative then it is changed into a positive one. If the value is positive then it gets changed into a negative one.
Ceiling Rounds up a number to the next whole number. For example, 3.142 is rounded up to 4.0.
Floor Rounds down a number to the previous whole number. For example, 3.142 is rounded down to 3.0.
Nearest Rounds a number to the nearest whole number. If the value is 0.5 or above it gets rounded up, otherwise it gets downed down.
Truncate Removes the decimal points from a number, leaving the whole number parts only. This is similar to Floor but it works differently with negative numbers.
Square Root Calculates the square root of a number.
Minimum Works out the smallest of two numbers.
Maximum Works out the largest of two numbers.
Copy Sign Copies the sign part of one number and gives it to another.

If you want to find out more about any of these instructions then take a look at them in the reference section.

Converting Types

You can convert the values from one type into another, but there are limitations and a number of different things to take into account. There are many instructions involved, too many to cover here, but I will go into the important ones and look at the details you need to be aware of.

i64 to i32

You can only copy the lower half of a 64 bit integer into a 32 bit integer, so there will be some loss of data. This is done using the i32.wrap_i64 instruction.

;; Push the i64 value onto the stack
i64.const 0x9349C4FC0A12BC62

;; This instruction pops the last item off the stack, converts it into an
;;  i32 number, and pushes the result onto the stack. Result = 0x0A12BC62
i32.wrap_i64

This takes the i64 number off the stack, converts it into an i32 number, and pushes it onto the stack. You will see that the higher part of the 64 bit number is gone.

i32 to i64

Copying an i32 number into an i64 type number can be done in two ways. You can treat the number as signed (can be negative) or unsigned. You use either the i64.extend_i32_s or i64.extend_i32_u instructions.

If the i32 number has its most significant bit set to 1 then it can be seen as being a negative number. Therefore when we convert it into an i64 type number, it also needs to become a negative number.

;; Push the i32 value onto the stack
i32.const 0xFFFFFFFF

;; This instruction pops the last item off the stack, converts it into an
;; i64 number, seeing it as a signed number, and pushes the result onto
;; the stack. Result = 0xFFFFFFFFFFFFFFFF
i64.extend_i32_s

;; Push the i32 value onto the stack
i32.const 0xFFFFFFFF

;; This instruction pops the last item off the stack, converts it into an
;; i64 number, seeing it as an unsigned number, and pushes the result onto
;; the stack. Result = 0x00000000FFFFFFFF
i64.extend_i32_u

This takes the same i32 number and converts it into an i64 data type. The first time this is done it sees the number as a signed value (-1) and when converting it into a 64 bit number that also becomes -1. You will notice that all the bits are 1, not just the lower 32 bits. The second time we are looking at the number as unsigned, as a positive number only, and therefore the higher part of the 64 bit number is set to zero, while the lower part matches the starting 32 bit number.

f64 to i32

You can only convert the whole number part of a floating point number, with everything after the decimal point being discarded. It also needs to be within the range of values an integer can contain. This depends on whether the integer is seen as signed (can be negative) or unsigned (positive value only). This is done using the i32.trunc_f64_s and i32.trunc_f64_u instructions.

;; Push the f64 value onto the stack
f64.const -123.456

;; This instruction pops the last item off the stack, converts it into an
;; i32 number, seeing it as a signed number, and pushes the result onto
;; the stack. Result = -123, 0xFFFFFF85
i32.trunc_f64_s

;; Push the f64 value onto the stack
f64.const 123.456

;; This instruction pops the last item off the stack, converts it into an
;; i32 number, seeing it as an unsigned number, and pushes the result onto
;; the stack. Result = 123, 0x0000007B
i32.trunc_f64_u    

The first part is taking the negative floating point number, converting it into a 32 bit number, one that is being viewed as a signed number. This will create a negative integer value. The second part is converting a positive number and treating it as unsigned.

If you try to convert a negative floating point number into an unsigned integer then you will get a runtime error. This is because the number was not within the range of an unsigned integer. You will also get this error if the number is too large to fit.

These limits can be handled by using the i32.trunc_sat_f64_s and i32.trunc_sat_f64_u instructions. If the number goes over the limit range then instead of throwing a runtime error, it converts the number to the maximum range value. For example, if the floating point number is -123.45 and we convert it unsigned, then instead of throwing an error, the result is set to zero, as that is the range limit.

i32 to f64

Converting an integer value into a floating point number can be done using the f64.convert_i32_s and f64.convert_i32_s instructions. Because the integer can be seen as either signed (can be negative) or unsigned (positive only), there are two different instructions to handle both.

;; Push the i32 value onto the stack
i32.const 0xFFFFFFFF

;; This instruction pops the last item off the stack, converts it into
;; a f64 number, seeing it as a signed number, and pushes the result
;; onto the stack. Result = -1.0
f64.convert_i32_s

;; Push the i32 value onto the stack
i32.const 0xFFFFFFFF

;; This instruction pops the last item off the stack, converts it into
;; a f64 number, seeing it as an unsigned number, and pushes the result
;; onto the stack. Result = 4294967295.0
f64.convert_i32_u  

We are converting the same 32 bit integer value, however, the first time we are seeing the value as a signed value, -1 in this case. Afterwards we are viewing the number as unsigned, with the value of 4294967295. The end result is different in both cases.