The ECMAScript proposal “Integer: Arbitrary precision integers in JavaScript” by TC39 member Daniel Ehrenberg is currently at stage 2. This blog post gives an overview.
Update 2017-03-21: New section “FAQ”.
Given that the ECMAScript standard only has a single type for numbers (64-bit floating point numbers) it’s amazing how far JavaScript engines were able to go in their support for integers: fractionless numbers that are small enough are stored as integers (usually within 32 bits, possibly minus bookkeeping information).
However, JavaScript can only safely represent integers with up to 53 bits plus a sign. Sometimes, you need more bits. For example:
The proposal is about adding a new primitive type for big ints to JavaScript. Given that they will be the only type for integers, the type will simply be called Integer.
Core parts of the proposal are:
n
. For example: 123n
typeof
returns 'integer'
for Integer values:> typeof 123n
'integer'
+
and *
are overloaded and work with Integers. The number of bits used to store values is increased as necessary, automatically.Integer
for Integers, which is similar to Number
for Numbers and other wrapper constructors.Next, we will look at a first example and then will examine all aspects of the proposal in detail.
This is what using Integers looks like (example taken from the proposal’s readme):
/**
* Takes an Integer as an argument and returns an Integer
*/
function nthPrime(nth) {
function isPrime(p) {
for (let i = 2n; i < p; i++) {
if (p % i === 0n) return false;
}
return true;
}
for (let i = 2n; ; i++) {
if (isPrime(i)) {
if (--nth === 0n) return i;
}
}
}
Like Number literals, Integer literals support several bases:
123n
0xFFn
0b1101n
0o777n
Negative integers are produced by prefix the unary minus operator: -0123n
The general rule for binary operators is:
You can’t mix Numbers and Integers: If one operand is an Integer, the other one can’t be a Number.
If you do mix them, a TypeError
is thrown:
> 2n + 1
TypeError
The reason for this rule is that there is no general way of coercing a Number and an Integer to a common type: Numbers can’t represent Integers beyond 53 bits, Integers can’t represent fractions. Therefore, the exceptions warn you about typos that could change the results of computations in unexpected ways.
To see why, let’s look at an example: 9007199254740991 is the highest integer that Numbers can represent safely. You can see that if you try adding 1 to the “unsafe” 9007199254740992
> 9007199254740992 + 1
9007199254740992
If 9007199254740992n + 1
were interpreted as a Number (due to coercion), you would get wrong results.
Additionally, disallowing mixed operand types keeps operator overloading simple, which is helpful should overloading be extended further in the future.
The following sections explain what operators are available for Integers.
Binary +
, binary -
, *
, **
work as expected.
/
, %
round towards zero (think Math.trunc()
).
> 1n / 2n
0n
Ordering operators <
, >
, >=
, <=
work as expected.
Unary -
works as expected. Unary +
is not supported for Integers, because much code (incl. asm.js) relies on it coercing its operand to Number.
For bit operators, A negative sign is interpreted as an infinite two’s complement. E.g.:
-1
is ...111
(ones extend infinitely to the left)-2
is ...110
(ones extend infinitely to the left)That is, a negative sign is more of an external flag and not represented as an actual bit.
The following bit operators exist:
Bitwise operators |
, &
, ^
for Integer work analogously to their Number versions.
Signed shift operators <<
, >>
for Integers work analogously to their Number versions. Note that here, too, both operands need to be Integers.
Bit operators for Numbers limit their operands to 32 bits. All operators (except for unsigned right shift >>>
) interpret the highest (31st) bit as a sign:
> Math.pow(2,30) | 0
1073741824
> Math.pow(2,31) | 0
-2147483648
> (Math.pow(2,32)-1) | 0
-1
You can shift a positive Number left so that the highest (31st) bit is set and it becomes negative:
> Math.pow(2,30) << 1
-2147483648
With Integers, that can never happen, because they are not limited to specific number of bits and there is therefore no sign bit.
There is no unsigned right shift operator >>>
for Integers, because its semantics are: “shift in” a zero, replace the highest bit with a zero. First, there is no highest bit. Second, with the infinite sequence of ones prefixing negative values, you’d have to insert a zero somewhere, which makes no sense. Thus, preserving the sign is the natural (and only) thing to do for Integers and there is no >>>
operator.
One last illustration of how negative bit operands work: For both Numbers and Integers, however often you signed-shift -1
to the right, the result is always -1
:
> -1 >> 20
-1
> -1n >> 20n
-1n
Lenient equality (==
) and inequality (!=
) are coercing operators, which makes them difficult to adapt to Integers. At the moment, comparing Numbers and Integers throws an exception:
> 0n == 0
TypeError
Alas, lenient equality coerces booleans to Numbers, meaning that exceptions are thrown, too:
> 0n == false
TypeError
Strict equality (===
) and inequality (!==
) only consider values to be equal if they have the same type. Therefore, adapting them to Integers is simple:
> 0n === 0
false
Integer
Similar to Numbers, Integers have the associated wrapper constructor Integer()
. It works as follows:
Integer(x)
: convert arbitrary values x
to Integer. This works similarly to Number()
, but:
TypeError
is thrown if x
is either null
or undefined
.NaN
for Strings that don’t represent Integers, a SyntaxError
is thrown.new Integer()
: throws a TypeError
.
This is what using Integer()
looks like:
> Integer(undefined)
TypeError
> Integer(null)
TypeError
> Integer(false)
0n
> Integer(true)
1n
> Integer(123)
123n
> Integer('123')
123n
> Integer('123n')
SyntaxError
> Integer('abc')
SyntaxError
Integer
methods Integer.prototype
holds the methods “inherited” by primitive Integers:
Integer.prototype.toLocaleString(reserved1?, reserved2?)
Integer.prototype.toString(radix?)
Integer.prototype.valueOf()
Integer.asUintN(width, theInt)
Casts theInt
to width
bits (unsigned). This influences how the value is represented internally.
Integer.asIntN(width, theInt)
Casts theInt
to width
bits (signed).
Integer.parseInt(string, radix?)
Works similarly to Number.parseInt()
, but throws a SyntaxError
instead of returning NaN
:
> Integer.parseInt('9007199254740993', 10)
9007199254740993n
> Integer.parseInt('abc', 10)
SyntaxError
For comparison, this is what Number.parseInt()
does:
> Number.parseInt('9007199254740993', 10)
9007199254740992
> Number.parseInt('abc', 10)
NaN
Casting allows you to create integer values with a specific number of bits. If you want to restrict yourself to just 64-bit integers, you have to always cast:
const int64a = Integer.asUintN(64, 12345n);
const int64b = Integer.asUintN(64, 67890n);
const result = Integer.asUintN(64, int64a * int64b);
This table show what happens if you convert Integers to other primitive types:
Convert to | Explicit conversion | Coercion (implicit conversion) |
---|---|---|
boolean | Boolean(0n) → false |
!0n → true |
Boolean(int) → true |
!int → false |
|
number | Number(int) → OK |
+int → TypeError |
string | String(int) → OK |
''+int → OK |
Still under discussion: Should the result of String()
applied to an Integer should have the suffix 'n'
? At the moment, it works like this:
> String(123n)
'123'
Integers make it possible to add 64 bit support to Typed Arrays and DataViews:
Uint64Array
Int64Array
DataView.prototype.getInt64()
DataView.prototype.getUint64()
Integers in JSON data will probably be handled similarly to other unsupported data such as symbols:
> JSON.stringify(123n)
undefined
> JSON.stringify([123n])
'[null]'
There will probably be a library with functions and constants for Integers (think Math
, but for Integers instead of Numbers).
Implementors of JS engines are confident that we will only need a single type for integers and that it will be fast enough for all use cases. But one could, in principle, introduce subtypes of Integer
(Uint64
, Uint8
, ...).
We’ll probably eventually see support for:
The following features may be added to JavaScript and could be based on these mechanisms.
My recommendations:
Array.prototype.forEach()
Array.prototype.entries()
All existing web APIs return and accept only Numbers and will only upgrade to Integer on a case by case basis
That would probably break existing code that may depend, possibly in subtle ways on Numbers being doubles (64-bit floating point numbers).
It is great to see support for integers beyond 53 bits being planned for JavaScript. Using a single type for integers is an interesting experiment. It’d be great if it worked out. Once again, JavaScript engines would do “the right thing” for programmers. Just like already do for smaller integers, Arrays, constructors (hidden classes...), etc.
With Integers, we get a glimpse at what JavaScript would be like if it had had exceptions from the start (they were introduced in ES3): using the wrong operands for some of the operators throws exceptions now.
Feedback to the proposal is best given by filing an issue on GitHub.
Acknowledgement. Thanks to Daniel Ehrenberg for reviewing this blog post.