Javascript's Shift right with zero-fill operator (>>>) yielding unexpected result
When you execute (-1 >>> 0)
you are executing an unsigned right shift. The unsigned here is key. Per the spec, the result of >>>
is always unsigned. -1
is represented as the two's compliment of 1
. This in binary is all 1
s (In an 8 bit system it'd be 11111111
).
So now you are making it unsigned by executing >>> 0
. You are saying, "shift the binary representation of -1
, which is all 1
s, by zero bits (make no changes), but make it return an unsigned number.” So, you get the value of all 1
s. Go to any javascript console in a browser and type:
console.log(2**32 - 1) //4294967295
// 0b means binary representation, and it can have a negative sign
console.log(0b11111111111111111111111111111111) //4294967295
console.log(-0b1 >>> 0) //4294967295
Remember 2 **
any number minus 1
is always all ones in binary. It's the same number of ones as the power you raised two to. So 2**32 - 1
is 32 1
s. For example, two to the 3rd power (eight) minus one (seven) is 111
in binary.
So for the next one (-1 >>> 32) === (2**32 - 1)
.... let's look at a few things. We know the binary representation of -1
is all 1
s. Then shift it right one digit and you get the same value as having all 1
s but precede it with a zero (and return an unsigned number).
console.log(-1 >>> 1) //2147483647
console.log(0b01111111111111111111111111111111) //2147483647
And keep shifting until you have 31 zeros and a single 1
at the end.
console.log(-1 >>> 31) //1
This makes sense to me, we have 31 0
s and a single 1
now for our 32 bits.
So then you hit the weird case, shifting one more time should make zero right?
Per the spec:
6.1.6.1.11 Number::unsignedRightShift ( x, y )
Let lnum be ! ToInt32(x).
Let rnum be ! ToUint32(y).
Let shiftCount be the result of masking out all but the least significant 5 bits of rnum, that is, compute rnum & 0x1F.
Return the result of performing a zero-filling right shift of lnum by shiftCount bits. Vacated bits are filled with zero. The result is an unsigned 32-bit integer.
So we know we already have -1
, which is all 1
s in twos compliment. And we are going to shift it per the last step of the docs by shiftCount
bits (which we think is 32). And shiftCount
is:
Let shiftCount be the result of masking out all but the least significant 5 bits of rnum, that is, compute rnum & 0x1F.
So what is rnum & 0x1F
? Well &
means a bitwise AND
operation. lnum
is the number left of the >>>
and rnum
is the number right of it. So we are saying 32 AND 0x1F
. Remember 32 is 100000
. 0x
is hexadecimal where each character can be represented by 4
bits. 1
is 0001
and F is 1111
. So 0x1F
is 00011111
or 11111
(31
in base 10, 2**5 - 1
also).
console.log(0x1F) //31 (which is 11111)
32: 100000 &
0x1F: 011111
---------
000000
The number of bits to shift if zero. This is because the leading 1
in 32
is not part of the 5
most significant bits! It's too many numbers. So we take 32 1
s and shift it zero bits! That's why. The answer is still 32 1
s.
On the example -1 >>> 31
this made sense because 31 is <= 5
bits. So we did
31: 11111 &
0x1F: 11111
-------
11111
And shifted it 31
bits.... as expected.
Let's test this further.... let's do
console.log(-1 >>> 33) //2147483647
console.log(-1 >>> 1) //2147483647
That makes sense, just shift it one bit.
33: 100001 &
0x1F: 011111
---------
00001
So, go over 5
bits with a bitwise operator and get confused. Want to play stump the dummy with a person who hasn't researched the ECMAScript to answer a stackoverflow post? Just ask why are these the same.
console.log(-1 >>> 24033) //2147483647
console.log(-1 >>> 1) //2147483647
Well of course it's because
console.log(0b101110111100001) // 24033
console.log(0b000000000000001) // 1
// ^^^^^ I only care about these bits!!!
When you do (-1 >>> 0)
, you are turning the sign bit into zero while keeping the rest of the number the same, therefore ending up as 2**32 - 1
.
The next behaviour is documented in the ECMAScript specification. The actual number of shifts is going to be "the result of masking out all but the least significant 5 bits of rnum, that is, compute rnum & 0x1F
".
Since 32 & 0x1F === 0
, both of your results will be identical.