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 1s (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 1s, by zero bits (make no changes), but make it return an unsigned number.” So, you get the value of all 1s. 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 1s. 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 1s. Then shift it right one digit and you get the same value as having all 1s 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 0s 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 1s 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 1s and shift it zero bits! That's why. The answer is still 32 1s.

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.