I.e., V8 JavaScript is a Smalltalk derived engine. (1980s - present) Lisp and Smalltalk engines support multi-precision arithmetic using <LargeInteger> sometimes called <BigInt>. Spoiler, the Dart team at Google is largely a bunch of ex-Smalltalkers bringing their experience together into the JS space.
These types of numbers have unlimited precision and are typically used as building blocks to provide <Rational:Fraction> objects whose numerator and denominator can be any type of number including a <BigInt>. With that one can represent real-numbers, imaginary-numbers, and do so with perfect precision on irrational numbers like (1/3).
Note: I'm a long time implementer and developer of Smalltalk, JS and other languages and their engines and frameworks.
If done appropriately <BigInt> for multi-precision arithmetic as a standard feature of JavaScript will open the door to a tremendous suite of operations, including native efficient cryptography (which is easy to do with multi-precision numbers).
For example, in one of my 1998 smalltalk engines, on a 2.3GHz cpu I just ran:
[10000 factorial] millisecondsToRun => 59ms
10000 factorial asString size => 35660 digits
[20000 factorial] millisecondsToRun => 271ms
20000 factorial asString size => 77338 digits
Defined as: (illustrating <BigInt>
multi-precision in action)
factorial
"Return the factorial of <self>."
| factorial n |
(n := self truncate) < 0 ifTrue: [^'negative factorial' throw].
factorial := 1.
2 to: n do:
[:i |
factorial := factorial * i.
].
^factorial
The V8 engine from Lars Bak (a contemporary of mine) work is derived from Animorphic Smalltalk from David Ungar's SELF work derived from Smalltalk-80, and subsequently evolved into the JVM, and redone by Lars for Mobile emerging later as the V8 engine foundation.
I mention that because both Animorphic Smalltalk and QKS Smalltalk support type-annotations which enable the engine and tools to reason about code in a similar way to that which TypeScript has attempted for JavaScript.
That annotation hinting and its use by the language, tools, and runtime engines offers the capability to support multi-methods (rather than double dispatch) needed to support multi-precision arithmetic type-promotion and coercion rules properly.
Which, in turn, is key to supporting 8/16/32/64 int/uints and many other numeric types in a coherent framework.
Multi-method <Magnitude|Number|UInt64>
examples from QKS Smalltalk 1998
Integer + <Integer> anObject
"Handle any integer combined with any integer which should normalize
away any combination of <Boolean|nil>."
^self asInteger + anObject asInteger
-- multi-method examples --
Integer + <Number> anObject
"In our generic form, we normalize the receiver in case we are a
<Boolean> or <nil>."
^self asInteger + anObject
-- FFI JIT and Marshaling to/from <UInt64>
UInt64 ffiMarshallFromFFV
|flags| := __ffiFlags().
|stackRetrieveLoc| := __ffiVoidRef().
""stdout.printf('`n%s [%x]@[%x] <%s>',thisMethod,flags,stackRetrieveLoc, __ffiIndirections()).
if (flags & kFFI_isOutArg) [
"" We should handle [Out],*,DIM[] cases here
"" -----------------------------------------
"" Is this a callout-ret-val or a callback-arg-val
"" Is this a UInt64-by-ref or a UInt64-by-val
"" Is this an [Out] or [InOut] callback-arg-val that needs
"" to be updated when the callback returns, if so allocate callback
"" block to invoke for doing this on return, register it as a cleanup hook.
].
^(stackRetrieveLoc.uint32At(4) << 32) | stackRetrieveLoc.uint32At(0).
-- <Fraction> --
Fraction compareWith: <Real> aRealValue
"Compare the receiver with the argument and return a result of 0
if the received <self> is equal, -1 if less than, or 1 if
greater than the argument <anObject>."
^(numerator * aRealValue denominator) compareWith:
(denominator * aRealValue numerator)
Fraction compareWith: <Float> aRealValue
"Compare the receiver with the argument and return a result of 0
if the received <self> is equal, -1 if less than, or 1 if
greater than the argument <anObject>."
^self asFloat compareWith: aRealValue
-- <Float> --
Float GetIntegralExpAndMantissaForBase(<[out]> mantissa, <const> radix, <const> mantissa_precision)
|exp2| := GetRadix2ExpAndMantissa(&mantissa).
if(radix = 2) ^exp2.
|exp_scale| := 2.0.log(radix).
|exp_radix| := exp2 * exp_scale.
|exponent| := exp_radix".truncate".asInteger.
if ((|exp_delta| := exp_radix - exponent) != 0) [
|radix_exp_scale_factor| := (radix.asFloat ^^ exp_delta).asFraction.
"" Limit it to the approximate precision of a floating point number
if ((|scale_limit| := 53 - mantissa.highBit - radix.highBit) > 0) [
"" Compute the scaling factor required to preserve a reasonable
"" number of precision digits affected by the exponent scaling
"" roundoff losses. I.e., force mantissa to roughly 52 bits
"" minus one radix decimal place.
|mantissa_scale| := (scale_limit * exp_scale).ceiling.asInteger.
mantissa_scale timesRepeat: [mantissa :*= radix].
exponent :-= mantissa_scale.
] else [
"" If at the precision limit of a float, then check the
"" last decimal place and follow a rounding up rule
if(exp2 <= -52 and: [(mantissa % radix) >= (radix//2)]) [
mantissa := (mantissa // radix)+1.
exponent :+= 1.
].
].
"" Scale the mantissa by the exp-delta factor using fractions
mantissa := (mantissa * radix_exp_scale_factor).asInteger.
].
"" Normalize to remove trailing zeroes as appropriate
while(mantissa != 0 and: [(mantissa % radix) = 0]) [
exponent :+= 1.
mantissa ://= radix.
].
^exponent.
I would expect that some similar patterns will begin to emerge for JavaScript support of UIn64/Int64 and other structural or numeric types as <BigInt> evolves.