Int type has some curious limitations.
Because JS only supports floats, implementations of addition and multiplication is currently implemented as
(a + b) | 0 and
(a * b) | 0 respectively (and
(a - b) | 0 for subtraction).
This ‘works fine’ for small numbers, but when the result overflows (or underflows) the 2^-31…2^31-1 range, the result is ill-defined (as in: unexpected and difficult to emulate in any non-JS target language).
There is this issue about replacing
(a * b) | 0 with Math.imul which would turn the operation into proper wrapping multiplication.
At the same time, some other compiler backends define
Ints and the operations on them differently:
Purerl (compiling to Erlang) uses Erlang’s unbounded arbitrary-size integer type for
Int. (no overflow possible)
Pscpp (compiling to C++) uses the C++ primitive
inttype (which on all but the most archaic C++ targets is 32-bits), and 32-bit wrapping addition/multiplication is used.
Psgo (compiling to go) uses the go primitive
inttype (which is 32-bits on 32-bit architectures and 64-bits on 64-bit architectures), and wrapping addition/multiplication is used.
Purenix (compiling to Nix) uses the Nix
inttype (which is 64 bits) and 64-bit wrapping addition/multiplication is used.
- (For completion’s sake) Haskell’s
Int’s exact range is implementation-defined but it is at least 30 bits. (and there are separate Int32 and Int64 types as well). Arithmetic wraps.
This situation can result in code that is not portable between backends, and the resulting problems are of the ‘sneaky’ kind since rather than being caught by type errors the results of arithmetic is slightly off – but only if sufficiently large numbers are used.
People seem to be regularly bitten by this (#2921, #3145).
What can we do to improve this situation? A couple ideas come to mind:
Be more clear about what the range of
- 32-bits on all targets?
- Fully implementation defined (potentially unbounded)? (Note that this makes code using
Boundedinstance to check whether certain operations can be performed incompilable on unbounded targets)
- Implementation-defined but always
Specify what should happen on overflow/underflow of arithmetic:
- ‘output is implementation-defined’. (AKA document the current situation)
- 'output is exactly what
(a * b) | 0in JS does (requiring non-trivial changes to all other compiler backends)
- ‘output wraps’ (requiring a change to the JS compiler backend)
- are there more possibilities?