Let's decide the semantics of `Int` operations

The native 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 int type (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 int type (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 int type (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:

  1. Be more clear about what the range of Int should be:

    • 32-bits on all targets?
    • Fully implementation defined (potentially unbounded)? (Note that this makes code using Bounded instance to check whether certain operations can be performed incompilable on unbounded targets)
    • Implementation-defined but always Bounded ?
  2. Specify what should happen on overflow/underflow of arithmetic:

    • ‘output is implementation-defined’. (AKA document the current situation)
    • 'output is exactly what (a * b) | 0 in 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?
8 Likes