Giter Club home page Giter Club logo

proposal-bigint-math's Introduction

BigInt Math for JavaScript

ECMAScript Stage-1 Proposal. J. S. Choi, 2021.

Description

(A formal draft specification is available.)

BigInts are important for a myriad of mathematical, financial, scientific, and timing applications (such as in the Node.js process.hrtime.bigint API), and they have been therefore a valuable addition to JavaScript since their standardization in ES 2021.

Several built-in Math functions would make sense with BigInts, yet they still do not support them; they only support regular floating-point JavaScript Numbers. This proposal extends those functions’ behavior to accept BigInts:

abs
sign
sqrt
pow *

min †
max †

* pow does not accept mixed types. pow(4, 2n) will throw a TypeError.

 min and max accept mixed numeric types:
min(0, 1n, -1) evaluates to 0,
and max(0, 1n, -1) evaluates to 1n.
This is well defined because < is well defined over mixed numeric types; there is no loss of precision.

When Math.min and Math.max receive values of different numeric types that nevertheless have equivalent mathematical values, then min prefers the leftmost value and max prefers the rightmost value. For example, Math.min(0, 0n) is 0 and Math.max(0, 0n) is 0n. (See issue #3.)

Philosophy

The philosophy is to be consistent with the precedents already set by the language. These precedents include the following five rules:

  1. BigInts and Numbers are not semantically interchangeable. It is important for the developer to reason about them differently.
  2. But, for ease of use, many (but not all) numeric operations (such as division / and exponentiation **) are type overloaded to accept both Numbers and BigInts.
  3. These type-overloaded numeric operations cannot mix Numbers and BigInts, with the exception of comparison operations.
  4. Some numeric operations are not overloaded (such as unary +). The programmer has to remember which operations are overloaded and which ones are not.
  5. asm.js is still important, and operations on which it depends are not type overloaded.

In this precedent, only syntactic operators are currently considered as math operations. We extend this precedent such that Math methods are also considered math operations.

Vision

This initial proposal overloads only a few first Math methods. The vision is that this proposal would open up the way to new proposals that would further extend Math with type-overloaded methods. These may include:

Excluded Math

Similarly to how some numeric operators are not overloaded (such as unary +), many Math functions that would not make sense with BigInts are excluded from this proposal. These include:

Math method Exclusion reason
acos Transcendental: very difficult to calculate when large
acosh Transcendental
asin Transcendental
asinh Transcendental
atan Transcendental
atan2 Transcendental
atanh Transcendental
cbrt No known use case
ceil No known use case; Math.ceil(3n / 2n) == 1 may be surprising
clz32 No known use case
cos Transcendental
cosh Transcendental
exp Transcendental
expm1 Transcendental
floor No known use case
fround Returns floating-point numbers by definition
hypot No known use case
imul No known use case; may complicated asm.js (see issue #9)
log Transcendental
log10 Truncation may be surprising; no known use case
log2 Truncation may be surprising; deferred to future bitLength proposal
log1p Transcendental
random No conceptual integer-only analogue
round No known use case; Math.round(3n / 2n) == 1 may be surprising
sin Transcendental
sinh No known use case
tan Transcendental
tanh Transcendental
trunc No known use case

proposal-bigint-math's People

Contributors

domenic avatar jakearchibald avatar js-choi avatar probins avatar robpalme avatar styfle avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

proposal-bigint-math's Issues

General philosophy and vision

Original post

Spinning this out of #8 (comment) and #9 (comment).

There are two dueling philosophies we could take for this proposal.

  1. BigInts and Numbers should always be interchangeable by default, unless there’s a strong reason they should not be (like precision loss or computational intractability).

    “It’s weird and confusing that they’re not already more interchangeable.”

  2. BigInts and Numbers should not be interchangeable by default. Floating point and arbitrary-precision integers are fundamentally different. The choice of which to use should be thought through by the programmer.

    “We already can’t have most of Math work for BigInts due to intractability [see #4], so even if we would like them to be interchangeable by default, that intuition just can’t hold up in practice. We need specific use cases for each one.”

Note that both philosophies would agree to add support for BigInt sign, abs, min, and max: there are clear use cases for all of these. (And if a clear use case appears in the future for certain other functions, then even the second philosophy would agree that we can add it in a future proposal.)

The proposal’s philosophy so far has been the first one (so it currently includes floor, ceil, etc. in #8). But the engine implementers have concerns about that, and we’re open to changing it. It would be good to make which philosophy we choose—and why we chose it—explicit.

There are also some relevant snippets from the 2021-08 meeting notes; I’ll wait until they’re public before I put them here.

CC: @syg, @yulia, @ljharb, @michaelficarra, @waldemarhorwat, @littledan

Edit (2021-09-17): The current answer is: The philosophy is neither maximizing interchangeability or maximizing separation. We maximize consistency with precedent instead.

Possible different direction: `BigMathStrawman`

Discussions around this proposal have shown that extending Math functions with BigInt support is difficult (e.g.: transcendental functions are intractable; max/min run into surprisingly difficult "how to spec this?" questions for mixed inputs; etc), and while a few people have expressed support for getting a little closer to interchangeability of Numbers and BigInts, others have questioned the usefulness of such efforts.

At the same time, there is consistent demand for certain new, BigInt-only functions. Therefore, I'm inclined to think that bringing those into the language might be a "juicier" (more useful, more rewarding, more interesting) area for BigInt-related spec work.

In this very repository, issues #1, #2, and #12 are examples of such demand. The original BigInt proposal also collected four ideas for future proposals (with some overlap with the former set).

That leads to the following list of functions that could be spec'ed:

  • bitLength (subsumes "truncating log2")
  • expMod (combined exponentiation and modulus)
  • fromString(..., radix)
  • gcd (greatest common divisor)
  • modInv (modular multiplicative inverse)
  • toByteArray, fromByteArray

Of course, abs, sign, min, max as discussed here so far could also be included. (If they're even worth it; they're all one-liners.)

Where exactly to put these functions is an open question. I've used BigMathStrawman in the title in reference to an earlier discussion; probably the BigInt object is a good home for them, but there are alternatives.

The functions themselves also have various open questions. Some of them (in particular bitLength and *ByteArray, maybe the others too) need to decide how to handle negative BigInts. Some of them (expMod, gcd, modInv) can reasonably be implemented in userspace (which doesn't mean that there can be no value in adding them to the language), others (bitLength, fromString, to/fromByteArray) can get significant efficiency benefits from a native implementation (e.g. bitLength is at least O(log n) in userspace, but O(1) natively). Some of them could be renamed (expMod because it's conceptually an exp followed by a %, or modExp because it's a "modular exp"? modInv because it's short, or modularInverse because it's descriptive?) If this or another proposal decides to take on these functions, all those discussions can be had in detail.

(This is just a thought and an attempt to be constructive; feel free to close this issue if your mind is set that you don't want to go in this direction.)

BigInt hypot and the nullary case

Unlike with Math.max and min (see #3), the arguments of Math.hypot cannot mix regular numbers and BigInts.

This means that we have three choices:

  • We extend Math.hypot to accept BigInt arguments. Math.hypot() with no arguments remains +0 the regular number. We are okay with Math.hypot(...arrOfBigInts) returning +0 whenever arrOfBigInts is empty. (Otherwise, non-integer results truncate, like with BigInt division.)
  • We add a new Math.bigHypot that handles the nullary-arguments case (throwing a TypeError). (Otherwise, non-integer results truncate, like with BigInt division.)
  • We don’t support any BigInt hypot.

After hearing feedback from TC39 at the prior presentation, my inclination is with the first choice.

Polyfill volunteers?

I opened this Issue because:

  • I want to write both the polymorphic polyfill and the non-polymorphic polyfill (so everyone can test both alternatives)
  • I want to ask whether we should use ESM or IIFE format
  • Asking if there are more volunteers?
  • Should we make polyfills now, or do it after there's more consensus?

BigInt log10 or log2?

The current plan is to remove BigInt support from all transcendental functions. However, there are exact cases for log10 and log2.

Should we support Math.log10(100n) or Math.log2(16n)? My gut says yes, and we can truncate non-integer results, as with BigInt division.

BigInt `sqrt` and `cbrt`

I’m spinning this out of #13.

@waldemarhorwat:

We should keep sqrt and cbrt for BigInts because we have pow. The truncation towards zero behavior is unsurprising (it matches /) and mathematically useful, both directly and in various algorithms.

For example, if you want to compute an arbitrary-precision square root of a BigInt, the truncated square root provides a great first step of the algorithm. You then square the truncated square root, subtract it from the original number, and proceed with the algorithm.

For another example, if you want to compute a truncated square root of a BigInt to 2 decimal places, multiply your original BigInt by 10000n, take the truncated square root, and you'll get the answer times 100n.

Another example: Suppose you want to compute the square root of a BigInt n rounded to the nearest integer instead of truncated. This is how you'd do it:

roundedSqrt = (Math.sqrt(4n * n) + 1n)/2n

Combining the two examples above, here's how to compute the square root of a BigInt n rounded to nearest to two decimal places:

let t = (Math.sqrt(40000n * n) + 1n)/2n;
let i = t / 100n; // Integral part of result
let f = t % 100n; // 00-99 decimal part of result

sqrt and cbrt are just inverses of the most common cases of pow. It would be as weird to have one and not the other as it would be to have * but not /. They're easy and very lightweight to implement — the implementation cost of including them is trivial enough that it's not worth the effort to conduct developer surveys.

I'm afraid we're getting into analysis paralysis and design-by-voting rather than picking the simplest option, which is including the Math functions that mathematically make sense.

@jakobkummerow:

Suppose you want to compute the square root of a BigInt n rounded to the nearest integer

Then I suggest you do Math.round(Math.sqrt(Number(my_bigint))). In fact, if you're interested in results rounded to integer (or to two decimal places, for that matter), then your entire calculation is probably better off with Numbers.

sqrt and cbrt are just inverses of the most common cases of pow. It would be as weird to have one and not the other

That argument cuts both ways: pow is a legacy function rendered obsolete by the introduction of **, so there's little reason to extend it in any way (for BigInts or otherwise). So if the only reason to have sqrt is that we have pow, but the latter isn't really motivated other than by "because we could", then we might as well have neither of them.

the implementation cost of including them is trivial enough

From that claim, in turn, one could also conclude that it's perfectly fine to leave implementations to user space, especially as long as we know of no use cases.

@waldemarhorwat:

sqrt and cbrt are just inverses of the most common cases of pow. It would be as weird to have one and not the other

That argument cuts both ways: pow is a legacy function rendered obsolete by the introduction of **, so there's little reason to extend it in any way (for BigInts or otherwise). So if the only reason to have sqrt is that we have pow, but the latter isn't really motivated other than by "because we could", then we might as well have neither of them.

For what it’s worth, I would like to gently push back against the notion that pow is merely a “legacy function”. pow remains in use today in functional programming as a reducible and partially applicable function object. I would be quite surprised if it weren’t still being used in new JavaScript code today. It’s not like it’s the actually deprecated with statement.

And if pow is still being used in new code today, then it will remain surprising whenever it doesn’t act like **.

In addition, as long as we’re assuming that “BigInt sqrt is useful as long as BigInt pow is in the language”, I would argue that BigInt ** also does count as being “in the language”. Under the previous assumption, BigInt sqrt is useful as long as BigInt ** is in the language. sqrt being an inverse of ** should be just as important as sqrt being an inverse of pow.

@js-choi:

sqrt and cbrt are just inverses of the most common cases of pow. It would be as weird to have one and not the other

That argument cuts both ways: pow is a legacy function rendered obsolete by the introduction of **, so there's little reason to extend it in any way (for BigInts or otherwise). So if the only reason to have sqrt is that we have pow, but the latter isn't really motivated other than by "because we could", then we might as well have neither of them.

For what it’s worth, I would like to gently push back against the notion that pow is merely a “legacy function”. pow remains in use today in functional programming as a reducible and partially applicable function object. I would be quite surprised if it weren’t still being used in new JavaScript code today. It’s not like it’s the actually deprecated with statement.

And if pow is still being used in new code today, then it will remain surprising whenever it doesn’t act like **.

In addition, as long as we’re assuming that “BigInt sqrt is useful as long as BigInt pow is in the language”, I would argue that BigInt ** also does count as being “in the language”. Under the previous assumption, BigInt sqrt is useful as long as BigInt ** is in the language. sqrt being an inverse of ** should be just as important as sqrt being an inverse of pow.

@jakobkummerow:

To be accurate, let's keep the distinction between "sqrt is useful" (of which no evidence has been presented) and "it's weird not to have sqrt" (which is an opinion that some individuals have expressed).

@waldemarhorwat:

Then I suggest you do Math.round(Math.sqrt(Number(my_bigint))).

And then you'd sometimes get the wrong answer.

In fact, if you're interested in results rounded to integer (or to two decimal places, for that matter), then your entire calculation is probably better off with Numbers.

Yes, lots of things can be done using Numbers. That doesn't say anything about the cases where you want guaranteed precision and rounding/truncating behavior. I'm not interested in rehashing the debate about the general usefulness of BigInts.

the implementation cost of including them is trivial enough

From that claim, in turn, one could also conclude that it's perfectly fine to leave implementations to user space, especially as long as we know of no use cases.

One could reach an incorrect conclusion. I was referring to the amount of code this would take, which is minuscule — smaller than some of the comments on this thread. However, the knowledge required to do it correctly is quite specialized and not accessible to most users. Also, a user-space implementation would not work as well as a built-in one because it would not be able to take advantage of the internal representation.

BigInt log2

@waldemarhorwat’s integer-roots implementation requires a log2 implementation, which he shims together using .toString().length. He notes regarding BigInt log2, “This should be a built-in.”

log2 is very useful for getting the bit length of a number. I think we might want to add BigInt log2 back to the proposal.
An alternative is to wait for a new bitLength function that supported BigInts but…why would we do that when we already could extend log2?

Drop transcendental functions except log10 and log2

Basked on feedback from @yulia (Mozilla SpiderMonkey) and @syg (Google V8), we will be dropping all functions that can return transcendentals when given integers. There are no clear use cases and processing time would need to be huge. I tentatively included them in the first place anyway just in case anyone else had any use cases.

Open question: Whether to keep BigInt sqrt and cbrt and, if so, what to do with irrational roots Math.sqrt(101n). (Truncate like BigInt division? Implementation approximated?)

Also @waldemarhorwat.

README min() example is broken

The README states:

min(0, 1n, -1) evaluates to 0,

But -1 is less than 0, so presumably it means to say it evaluates to -1? Or maybe it should exclude the -1 argument entirely.

BigInt imul?

I know of no use case for imul accepting BigInts, but at least its mathematical behavior would be well defined and exact.

On the other hand, making imul polymorphic could make optimization for asm.js more difficult. See also the original BigInt proposal’s “Don’t break asm.js” section.

@ljharb has said—and I agree—that that BigInts and Numbers should always be interchangeable by default, unless that would cause silent precision loss – i.e., unless there's a strong reason they should not be (#8 (comment)).

Under this framework, it’d be good to know if a polymorphic imul would worsen asm.js optimization, which would count as a “strong reason [BigInts and Numbers] should not be [interchangeable]”. CC: @syg, @codehag.

How should Math.min/max(0, 0n) be ordered?

I suggest to add those methods to globalThis.BigIntMath.* so we can get rid of bigMin and bigMax.

And please consider the Decimal proposal, if we follow this naming approach, will we have decimalMin and decimalMax?

Potential use case: Radix conversion for exact double-to-decimal

An IEEE 754 binary floating-point number is represented as an integer significand raised to a binary base (2^e). In order to convert a double to a decimal, a radix conversion from base 2 to base 10 is required. The exact conversion requires math that exceeds the capacity of fixed-width integer types.

Some additional reading describing the double-to-decimal conversion process:

Thoughts on Math.sum/Math.product and implications for this proposal

I'm thinking about adding two new methods for doing sum/product. They'd take iterables of numbers: Math.sum([1, 2, 3]) === 6.

And of course if you pass Math.sum an empty array, you'd get 0 out. But then these methods can't really work with BigInts at all: you can't distinguish "an empty array of Numbers" from "an empty array of BigInts", so you can't tell whether 0 or 0n is the identity. And you can't reasonably have Math.sum([1n]) give 1n but also have Math.sum([]) give 0.

So I'm thinking there should be separate Math.sum / Math.bigSum methods, each of which only accepts values of one specific type.

If committee agrees with that analysis, maybe that informs the design of this proposal? Everything currently in this proposal can get away with polymorphism, but that's not going to be true for every possible method, so maybe it will make more sense to split methods into Number/BigInt versions even when we could theoretically combine them. (see also #14)

Alternatively, I suppose, we could have Number.sum and BigInt.sum. That would be pretty weird but it would at least avoid this tension.

Fast exponentiation for bigint + modulo

One of the more common things I've used when using bigints is wanting power + modulo, generally this involves writing my own fast exponentiation function (using exponentation by squaring), it would be nice for Math.pow to support the third argument when used with bigints to enable this.

Most languages I've looked at already support this (e.g. in Python via pow(...), Java by bigInteger.modPow, and so on) so there is a lot of prior art.

Methods’ home: Math object vs. BigInt object

@michaelficarra has suggested in #8 (comment) that we could put BigInt math methods in the BigInt object rather than the Math object.

Although this is not my first preference, we must seriously consider it as an option.

We already have polymorphic math operators (+ - * ** /). I think developers would expect polymorphic Math methods, too. However, this is only my own intuition and mental model as a developer, and other developers might feel differently.

We probably have to have hardened consensus on this before Stage 2. CC: @ljharb

Claim about "Web Performance API"

The README.md states:

BigInts are important for a myriad of mathematical, financial, and scientific applications (such as in the Web Performance API)

Can you substantiate that claim? I see no mention of BigInts in the overview or documentation, and searching doesn't turn up any related hits either.

Or maybe there are other quotable examples from this supposed "myriad"?

Polymorphic Math methods, backwards compatibility, and polyfills

Suppose code was written with the knowledge that the Math methods will throw exceptions if given BigInt values.

// example
function myNumberOnlyApiMethod(numArray) {
  // This method should throw an exception if any of the values are BigInts
  const maxNum = numArray.reduce((prev, cur) => Math.max(prev, cur))
  // do further Number-only logic here
}

If the implementation of some methods change to allow BigInt values instead, that code will not behave as designed.
If it were necessary to call a new method to get the new behavior, then the potential for this bug would be eliminated.

It would be good to explicitly consider whether it is better to incur this risk than to add a new method.

Parse string to BigInt with radix?

Current APIs:

  • parseInt() parses strings to Number with an arbitrary radix.
  • Number.prototype.toString() prints a Number to a string with an arbitrary radix.
  • BigInt.prototype.toString() prints a BigInt to a string with an arbitrary radix.

However, I cannot find a built-in function to parse a string to a BigInt with an arbitrary radix. Stack Overflow seems to agree.

Worth including in this proposal?

(Follow-up to #5)

Count leading zeroes

Where you omitted Math.clz32, you could replace that with Math.clzN(bigint, N), where the bigint is to be interpreted as an N-bit number.

In particular, Math.clzN(bigint, 64) could be accelerated on x64 and ARM, and if the bigint can't fit in a single 64-bit number, it can still be checked very efficiently by roughly this algorithm:

// For 32-bit - 64-bit will be similar
let m = Math.min(N, bigint.byteLength << 2)
let n = m & ~3
let i = 0

while (i < n) {
  let b = bigint.getUint32(i, true)
  if (b !== 0) {
    return (i << 3) + Math.clz32(b)
  }
  i += 4
}

if (i < m) {
  return Math.min(m, (i << 3) + Math.clz32(bigint.getUint32(i, true)))
}

return m

BigInt ceil, floor, round, and trunc?

I originally did not include BigInt ceil, floor, round, and trunc. However, someone from TC39 (I think it was @ljharb) suggested that we add them as identity functions.

Yesterday, there was some pushback from other TC39 members about this. As far as I can tell, this is bikeshedding and both choices are harmless, but we’ll have to make a decision someday anyway.

CC: @ljharb, @michaelficarra, @waldemarhorwat

sqrt/cbrt/log2/log10 and rounding

in my Chrome:
Math.cbrt(131329**3-1)
gives
131329
if Math.cbrt(131329n**3n-1n) gives 131328 then you cannot expect the equal result for all safe integers.

the same is for Math.sqrt(67108865**2 - 1) === 67108865
and Math.log2(2**49 - 1) === 49, Math.log10(10**15 - 2) === 15

The range for which this holds depends on the accuracy (precision?) of the Math.sqrt and Math.cbrt, for Math.sqrt, I think, it is 0.5ulps, while for Math.cbrt it is implementation defined and limited to some small amount of ulps (4) .

function value
Math.cbrt 131329**3 - 1 ~= 2**51.00847800299688
Math.sqrt 67108865**2 - 1 ~= 2**52.00000004299566
Math.log2 2**49 - 1
Math.log10 10**15 - 2 ~= 2**49.82892142331043

Test:

for (var i = 0; 2**i < 2**53; i++) console.log(i, Math.log2(2**i-1) < i, Math.log2(2**i)>=i)
for (var i = 0; 10**i < 2**53; i++) console.log(i, Math.log10(10**i-1) < i, Math.log10(10**i)>=i)
var i = 1;
while (i**3 < 2**53 && Math.cbrt(i**3 - 1) < i && Math.cbrt(i**3) >= i) {
  i++;
}
console.log(i);
var i = 1;
while (i**2 < 2**53 && Math.sqrt(i**2 - 1) < i && Math.sqrt(i**2) >= i) {
  i++;
}
console.log(i);

The `at` method of indexables should support `BigInt`s

I know this may be out of the scope of this proposal, but the fact that ['a', 'b', 'c'][x] works regardless of whether x is a BigInt or a Number while at throws an error just seems wrong. I don't know if there's any reason or justification about why this happens, but I hope polymorphism for at becomes a thing. Please let me know if I'm missing something

Why reuse Math namespace?

The creation of the Math namespace never made much sense in the first place. It also seems unnecessarily confusing that some methods work while others do not.

Instead, why not attach them directly to BigInt where they belong?

In the case of abs (for example), I'd recommend going with BigInt.abs for traditional static use like BigInt.abs(-123) and BigInt.prototype.abs for use as either foo.abs() or a literal (-123n).abs()

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.