by Christoph Michel

I found a bug in V8’s Exponentiation Operator

A4T1txSLDHURXjOhslt0TlBAbAFB0Jz03yEA
Photo by Ritchie Valens on Unsplash

I always thought that the new ES6 exponentiation operator x ** y was the same as Math.pow(x,y).

Indeed this is what the specification says about Math.pow:

Return the result of Applying the ** operator with base and exponent as specified in 12.6.4.

12.6.4 — Applying the ** Operator states that the result is implementation-dependent — but there should still be no discrepancy between ** and Math.pow.

However, evaluating the following in the current V8 JS Engine (Chrome / Node) results in this:

console.log('1.35 ** 92', 1.35 ** 92)                   // 978828715394.7672console.log('Math.pow(1.35, 92)', Math.pow(1.35, 92))   // 978828715394.767

The exponentiation operator ** returns a more accurate approximation.

But this is not the only weirdness with the exponentiation operator: Let’s try evaluating the same with variables (REPL) — it shouldn’t make any difference:

THt5NNcrKd1S9dd99OY4jUhPB0nw9wrHiY0K
const exponent = 92;console.log(`1.35 ** exponent`, 1.35 ** exponent)                   // 978828715394.767console.log('1.35 ** 92', 1.35 ** 92)                               // 978828715394.7672console.log(`Math.pow(1.35, exponent)`, Math.pow(1.35, exponent))   // 978828715394.767console.log('Math.pow(1.35, 92)', Math.pow(1.35, 92))               // 978828715394.767

But it does: 1.35 ** 92 differs from 1.35 ** exponent.

So what seems to be happening here is that the compiler processes the JS code 1.35 ** 92 which is already constant folded

This makes sense as V8 really compiles to machine code.

V8 increases performance by compiling JavaScript to native machine code before executing it, versus executing bytecode or interpreting it.

V8 works by first interpreting the JS code with their Ignition Interpreter. It does a second run with the TurboFan compiler optimizing the machine code.

TyMWLEdnZyqL2oDHhD8mWqiYH0vsL6vgjp9J
From Understanding V8’s bytecode

TurboFan now does constant folding. Its exponentiation algorithm has a better precision than the JIT compiler’s (Ignition) exponentiation algorithm.

If you try the same in other JS engines like Firefox’s SpiderMonkey, the result is a consistent value of 978828715394.767 among all computations.

Is it a bug?

I would say so, although it wasn’t severe in my code. But it’s still not following the spec that says Math.pow and ** should result in the same value.

If you’re transpiling the code with babel, x ** y is translated to Math.pow(x,y), which again leads to discrepancies between transpiled and untranspiled code. As we have seen, Math.pow(1.35, 92) is not being optimized (only operators seem to be optimized by V8). Therefore, 1.35 ** 92 results in different code when transpiled to ES5.

Using this bug and disregarding any clean code practices, we can write a nice function to determine if we’re running on Chrome (unless you transpile your code ?):

function isChrome() {    return 1.35 ** 92 !== Math.pow(1.35, 92)}

Still more readable than user agent strings. ?

Originally published at cmichel.io

9APsDGxF26LJJOgAusJLRrMPLw3zBSbLoP8u