You can manipulate integers using their bits instead of just their values.
There are a number of different bitwise instructions you can perform on i32
and i64
integer values.
They are a powerful tool that allows you to interact with data of any type and in many different ways.
Controlling data at the bit level is one of the key parts to programming and knowing how to do it is a must for any developer.
The AND bitwise operation takes all the bits from one integer and performs a logical AND with a second integer, bit by bit.
A | B | Result |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
Each bit in integer A is processed with the same bit in integer B.
For a logical AND operation, if both A and B are 1 then the result is 1, otherwise it will always be 0.
Both A AND B need to be 1 for the result to be 1.
To do this you need to use either the i32.and
or i64.and
instructions.
;; Push the i32 value 0b00001010 onto the stack i32.const 0x0A ;; Push the i32 value 0b00001100 onto the stack i32.const 0xC ;; Pop the last two i32 values off the stack, perform ;; logical AND operation and push the result onto ;; the stack i32.and ;; The stack contains an i32 with the value 0b0001000
The first step is to put both i32
integer values onto the stack.
It doesn't matter what order you do this, the result will be the same.
The second step is to call the i32.and
instruction, which pops both the values off the stack, performs the logical AND operation on them,
and then pushes the result onto the stack.
Let's take a closer look at what is happening with each bit in the two values.
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
First | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
Second | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
Result | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
We are only looking at the first 8 bits, all the other bits are zero. For bit 0 in the first and second number, they are both 0, therefore the AND operation will result in 0. For bit 1, the first number has a 1 but the second number has a 0, which means the result of the AND operation will be 0 again. Likewise, with bit 2, it's the other way round, the first number has a 0 and the second a 1, which again results in 0. However, with bit 3, both the first and second numbers have a 1, which when the AND operation is performed, will result in a 1.
Only bit 3 has a 1 result, all the others are 0. The AND operation is like performing a single AND on each bit of the two integer values.
The logical bitwise OR operation looks at each bit from one integer and checks it against the same bit from a second integer, seeing if either of them are 1 or not.
A | B | Result |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
A bit from integer A is compared to the same bit from integer B. With the logical OR operation, if either A or B, or both, are 1 then the result is 1. If both A and B are 0 then the result is 0. Either A OR B needs to be 1 for the result to be 1. This is done using the i32.or or the i64.or instructions.
;; Push the i32 value 0b00001010 onto the stack i32.const 0x0A ;; Push the i32 value 0b00001100 onto the stack i32.const 0xC ;; Pop the last two i32 values off the stack, perform ;; logical OR operation and push the result on to ;; the stack i32.or ;; The stack contains an i32 with the value 0b0001110
To begin with, the i32
integer values both need to be pushed onto the stack.
The order does not matter, the result will be the same.
The next part is to call the i32.or
instruction, which pops the integer values from the stack, performs the OR operation on them both,
and finally pushes the end result on the stack.
Let us look more closely at the bits in both numbers and see what is happening.
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
First | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
Second | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
Result | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 |
The only bits we are interested in here are the first 8 bits. Starting off with bit 0, both the first and second numbers have a bit value of 0, which when an OR operation is performed, will have the result of 0. With bit 1, the first integer has the value 1, and the second 0, therefore when the OR operation is called, the end result is 1. This is the same with bit 2, but the first integer has the value 0, with the second being 1. Now, with bit 3, both the first and second integers have the value 1, making the OR operation also result in 1.
The end result is that bits 3, 2 and 1 (but not bit 0) are set to 1.
The bitwise XOR operation is similar to the OR operation, except that if both bits are 1, the result is 0. Therefore, for each bit from one integer, it checks the same bit from a second integer, and compares them together, seeing if either of them are 1 or not (but not both).
A | B | Result |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
The same bit from A is compared to a bit from B.
Using the XOR logical operation, if either bit A or B are 1 then the result is set to 1.
If A and B are the same, either both 0 or both 1, then the result will be 0.
This is performed using the i32.xor
or the i64.xor
instructions.
;; Push the i32 value 0b00001010 onto the stack i32.const 0x0A ;; Push the i32 value 0b00001100 onto the stack i32.const 0xC ;; Pop the last two i32 values off the stack, perform ;; logical XOR operation and push the result on to ;; the stack i32.xor ;; The stack contains an i32 with the value 0b0000110
The same two i32
integers are pushed onto the stack as before.
Again, the order does not matter, as the result will be the same.
This is followed by calling the i32.xor
instruction, which takes the two integer values off the stack, performs the logical XOR operation,
and pushes the end result onto the stack.
We can see what is happening if we take a closer look at both numbers and their bits.
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
First | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
Second | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
Result | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 |
We only need to look at the first 8 bits. Looking at bit 0, both the numbers have a bit value of 0, which will result with 0 when you perform the XOR operation. With bit 1, the first number has a 1 and the second has a 0, therefore the XOR operation will give the 1 result. With bit 2 it is the other way round, the first number having 0 and the second 1, which when XOR is used, will again result as 1. Then with bit 3, the first and second both have 1, therefore the XOR operation result will become 0.
The end result is that bits 2 and 1 are set to 1, but bits 3 and 0 are not.
The instruction to shift bits to the left is not performing some type of logical gate operation on the bits.
Instead it is moving all the bits to the left by a given number of bits.
You can use either i32.shl
or i64.shl
instructions to perform the shift left operation depending on the data type you are using.
;; Push the i32 value 0b00000010 onto the stack i32.const 0x02 ;; Push the i32 value 1 onto the stack i32.const 1 ;; Pop the last two i32 values off the stack, shift the ;; first value left by the second amount and push the ;; result on to the stack i32.shl ;; The stack contains an i32 with the value 0b0000100
The first step is to push the value we want to shift onto the stack.
Then we push the amount we want to shift by, in this case 1 bit.
The order you push them onto the stack is important.
Finally we call the i32.shl
instruction to shift the value to the left by 1 bit and then push the result onto the stack.
Looking at the bits we can see what is happening.
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Before | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
After | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
All the bits have moved to the left by one bit. The new bit on the right, the new bit 0, will always be zero.
;; Push the i32 value 0b10110000_... onto the stack i32.const 0xB0000000 ;; Push the i32 value 2 onto the stack i32.const 2 ;; Pop the last two i32 values off the stack, shift the ;; first value left by the second amount and push the ;; result on to the stack i32.shl ;; The stack contains an i32 with the value 0b11000000_...
This time we are looking at the most significant bits, on the far left of the integer, to see what happens if we shift the bits beyond their range. Let's look at the bits in detail.
Bit | 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 |
---|---|---|---|---|---|---|---|---|
Before | 1 | 0 | 1 | 1 | 0 | 0 | 0 | 0 |
After | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
The bits have been moved left by two bits. The left most bits, 31 and 30, have been moved outside the range of a 32 bit integer, and have therefore been lost. You can only meaningfully shift the bits by a certain amount. Any more than the number of bits in the integer and you will just end up with zero, because all the bits in the integer will become lost.
There are two instructions for shifting bits to the right, one for signed numbers (can be negative) and one for unsigned numbers (positive values only).
When shifting to the right there will be new bits added to the left, but these can be either 0 or 1 depending on a number of factors.
You can use either i32.shr_s
or i32.shr_u
or the i64
equivalent.
;; Push the i32 value 0b00000100 onto the stack i32.const 0x04 ;; Push the i32 value 1 onto the stack i32.const 1 ;; Pop the last two i32 values off the stack, shift the ;; first value (unsigned) right by the second amount and ;; push the result on to the stack i32.shr_u ;; The stack contains an i32 with the value 0b0000010
First we push the value we want to shift right onto the stack.
After this we push the number of bits we want to shift to the right by.
Then finally we call the i32.shr_u
instruction to perform the shift right operation.
This will pop the two values off the stack and push the result onto the stack.
Let's take a look at what is happening at the bit level.
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Before | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
After | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
Every bit is being moved to the right by one. Everything seems simple enough.
;; Push the i32 value 0b00001101 onto the stack i32.const 0x0D ;; Push the i32 value 2 onto the stack i32.const 2 ;; Pop the last two i32 values off the stack, shift the ;; first value (unsigned) right by the second amount and ;; push the result on to the stack i32.shr_u ;; The stack contains an i32 with the value 0b0000011
This time we are shifting by 2 bits. Let's see what this looks like at the bit level.
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Before | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 1 |
After | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
All the bits have moved to the right by 2 bits. The bits 0 and 1 have been shifted to the right so far that they have gone past the start and have become lost. Bits 3 and 2 have moved to the start, replacing them.
Let's see what happens at the higher end of the integer number.
;; Shift right unsigned i32 value 0b10100000...00000000 by 1 i32.const 0xA0000000 i32.const 1 i32.shr_u ;; The result is 0b01010000...00000000 ;; Shift right signed i32 value 0b10100000...00000000 by 1 i32.const 0xA0000000 i32.const 1 i32.shr_s ;; The result is 0b11010000...00000000 ;; Shift right signed i32 value 0b01100000...00000000 by 1 i32.const 0x60000000 i32.const 1 i32.shr_s ;; The result is 0b00110000...00000000
In the first example we are using a number that could be seen as a negative number if it was used as a signed integer, but we are shifting it as an unsigned number. Doing this means that the new bits added to the left will always be 0.
In the second example we are using the same number, but this time we are shifting it with the signed instruction version. This instruction checks to see if the starting number is negative, and if so then any new bits added to the left will be set to 1. In this example the starting bit is 1, which indicates it is a negative number and therefore inserts the new bit on the left with a 1.
In the third example we use a different number, a positive one, even if it was seen as signed. We use the same signed shift instruction to move all the bits to the left by 1, but because the number is positive, the new inserted bits on the left will be set to 0.
Shifting the bits of a number to the right will end up losing bits on the right and inserting new bits on the left. The new bits on the left can be either 0 or 1 depending on the instruction you use and whether the value is negative or not.
Shifting bits either left or right will end up losing some bits on one end and having new bits added to the other. With the rotate instructions, nothing is lost, all the bits that would have been lost on one end are inserted into the new bits on the other. All that is happening is that the bits in the integer number are being rotated around in a cycle-like way.
;; Push the i32 value 0b11000010...00001010 onto the stack i32.const 0xC200000A ;; Push the i32 value 1 onto the stack i32.const 1 ;; Pop the last two i32 values off the stack, rotate the ;; first value left by the second amount and push the ;; result on to the stack i32.rotl ;; The stack contains an i32 with the value 0b10000100...00010101
First we push the value to be rotated onto the stack, followed by the number of bits we want to rotate it by.
The order this is done is important.
Then we call the i32.rotl
instruction to rotate the bits to the left.
This pops the parameters off the stack, performs the rotate left operation, and pushes the result onto the stack.
If we had shifted the bits to the left instead, we would have lost the left most bit, but when we rotate it, that bit value is inserted on the other end. Let's do that again but this time we will rotate it by 3 bits instead.
;; Rotate the i32 value 0b11000010...00001010 left by 3 bits i32.const 0xC200000A i32.const 3 i32.rotl ;; The stack contains an i32 with the value 0b00010000...01010110
The 110 bits on the left are moved to the right of the integer number. Let's do that again but move it to the right.
;; Rotate the i32 value 0b11000010...00001010 right by 3 bits i32.const 0xC200000A i32.const 3 i32.rotr ;; The stack contains an i32 with the value 0b01011000_0100...00000001
The 010 bits on the right are moved to the left part of the number.