From 9da401f49d0925444bddd755c124cc5ce129f1d6 Mon Sep 17 00:00:00 2001 From: Denis Pushkarev Date: Mon, 15 Apr 2024 07:15:04 +0700 Subject: [PATCH] add `Math.sumPrecise` https://github.com/tc39/proposal-math-sum --- packages/core-js-compat/src/data.mjs | 2 + .../src/modules-by-versions.mjs | 1 + packages/core-js/full/math/index.js | 2 + packages/core-js/full/math/sum-precise.js | 6 + .../modules/esnext.math.sum-precise.js | 162 ++++++++++++++++++ packages/core-js/proposals/math-sum.js | 3 + packages/core-js/stage/2.7.js | 1 + tests/compat/tests.js | 3 + tests/entries/unit.mjs | 2 + 9 files changed, 182 insertions(+) create mode 100644 packages/core-js/full/math/sum-precise.js create mode 100644 packages/core-js/modules/esnext.math.sum-precise.js create mode 100644 packages/core-js/proposals/math-sum.js diff --git a/packages/core-js-compat/src/data.mjs b/packages/core-js-compat/src/data.mjs index 531d2622dcef..53f77d807b78 100644 --- a/packages/core-js-compat/src/data.mjs +++ b/packages/core-js-compat/src/data.mjs @@ -2258,6 +2258,8 @@ export const data = { }, 'esnext.math.signbit': { }, + 'esnext.math.sum-precise': { + }, // TODO: Remove from `core-js@4` 'esnext.math.umulh': { }, diff --git a/packages/core-js-compat/src/modules-by-versions.mjs b/packages/core-js-compat/src/modules-by-versions.mjs index 708eb4cf86b2..5bd745ac0e2d 100644 --- a/packages/core-js-compat/src/modules-by-versions.mjs +++ b/packages/core-js-compat/src/modules-by-versions.mjs @@ -245,6 +245,7 @@ export default { 'es.set.is-superset-of.v2', 'es.set.symmetric-difference.v2', 'es.set.union.v2', + 'esnext.math.sum-precise', 'web.url.parse', ], }; diff --git a/packages/core-js/full/math/index.js b/packages/core-js/full/math/index.js index f1db3d46ac9d..ec931c708d52 100644 --- a/packages/core-js/full/math/index.js +++ b/packages/core-js/full/math/index.js @@ -1,5 +1,6 @@ 'use strict'; var parent = require('../../actual/math'); +require('../../modules/es.array.iterator'); require('../../modules/esnext.math.clamp'); require('../../modules/esnext.math.deg-per-rad'); require('../../modules/esnext.math.degrees'); @@ -9,6 +10,7 @@ require('../../modules/esnext.math.radians'); require('../../modules/esnext.math.scale'); require('../../modules/esnext.math.seeded-prng'); require('../../modules/esnext.math.signbit'); +require('../../modules/esnext.math.sum-precise'); // TODO: Remove from `core-js@4` require('../../modules/esnext.math.iaddh'); require('../../modules/esnext.math.isubh'); diff --git a/packages/core-js/full/math/sum-precise.js b/packages/core-js/full/math/sum-precise.js new file mode 100644 index 000000000000..fda5a72c0841 --- /dev/null +++ b/packages/core-js/full/math/sum-precise.js @@ -0,0 +1,6 @@ +'use strict'; +require('../../modules/es.array.iterator'); +require('../../modules/esnext.math.sum-precise'); +var path = require('../../internals/path'); + +module.exports = path.Math.sumPrecise; diff --git a/packages/core-js/modules/esnext.math.sum-precise.js b/packages/core-js/modules/esnext.math.sum-precise.js new file mode 100644 index 000000000000..59323fd78513 --- /dev/null +++ b/packages/core-js/modules/esnext.math.sum-precise.js @@ -0,0 +1,162 @@ +'use strict'; +// based on Shewchuk's algorithm for exactly floating point addition +// adapted from https://github.com/tc39/proposal-math-sum/blob/3513d58323a1ae25560e8700aa5294500c6c9287/polyfill/polyfill.mjs +var $ = require('../internals/export'); +var uncurryThis = require('../internals/function-uncurry-this'); +var iterate = require('../internals/iterate'); + +var $RangeError = RangeError; +var $TypeError = TypeError; +var $Infinity = Infinity; +var $NaN = NaN; +var abs = Math.abs; +var pow = Math.pow; +var push = uncurryThis([].push); + +var POW_2_1023 = pow(2, 1023); +var MAX_SAFE_INTEGER = pow(2, 53) - 1; +// exponent 11111111110, significand all 1s +var MAX_DOUBLE = 1.79769313486231570815e+308; // 2 ** 1024 - 2 ** (1023 - 52) +// exponent 11111111110, significand all 1s except final 0 +var PENULTIMATE_DOUBLE = 1.79769313486231550856e+308; // 2 ** 1024 - 2 * 2 ** (1023 - 52) +// exponent 11111001010, significand all 0s +var MAX_ULP = MAX_DOUBLE - PENULTIMATE_DOUBLE; // 1.99584030953471981166e+292, <- 2 ** (1023 - 52) + +var NOT_A_NUMBER = {}; +var MINUS_INFINITY = {}; +var PLUS_INFINITY = {}; +var MINUS_ZERO = {}; +var FINITE = {}; + +// prerequisite: abs(x) >= abs(y) +var twosum = function (x, y) { + var hi = x + y; + var lo = y - (hi - x); + return { hi: hi, lo: lo }; +}; + +// `Math.sumPrecise` method +// https://github.com/tc39/proposal-math-sum +$({ target: 'Math', stat: true, forced: true }, { + // eslint-disable-next-line max-statements -- ok + sumPrecise: function sumPrecise(items) { + var numbers = []; + var count = 0; + var state = MINUS_ZERO; + + iterate(items, function (n) { + if (++count >= MAX_SAFE_INTEGER) throw new $RangeError('Maximum allowed index exceeded'); + if (typeof n != 'number') throw new $TypeError('Value is not a number'); + if (state !== NOT_A_NUMBER) { + // eslint-disable-next-line no-self-compare -- NaN check + if (n !== n) state = NOT_A_NUMBER; + else if (n === $Infinity) state = state === MINUS_INFINITY ? NOT_A_NUMBER : PLUS_INFINITY; + else if (n === -$Infinity) state = state === PLUS_INFINITY ? NOT_A_NUMBER : MINUS_INFINITY; + else if ((n !== 0 || (1 / n) === $Infinity) && (state === MINUS_ZERO || state === FINITE)) { + state = FINITE; + push(numbers, n); + } + } + }); + + switch (state) { + case NOT_A_NUMBER: return $NaN; + case MINUS_INFINITY: return -$Infinity; + case PLUS_INFINITY: return $Infinity; + case MINUS_ZERO: return -0; + } + + var partials = []; + var overflow = 0; // conceptually 2**1024 times this value; the final partial is biased by this amount + var x, y, sum, hi, lo, tmp; + + for (var i = 0; i < numbers.length; i++) { + x = numbers[i]; + var actuallyUsedPartials = 0; + for (var j = 0; j < partials.length; j++) { + y = partials[j]; + if (abs(x) < abs(y)) { + tmp = x; + x = y; + y = tmp; + } + sum = twosum(x, y); + hi = sum.hi; + lo = sum.lo; + if (abs(hi) === $Infinity) { + var sign = hi === $Infinity ? 1 : -1; + overflow += sign; + + x = (x - (sign * POW_2_1023)) - (sign * POW_2_1023); + if (abs(x) < abs(y)) { + tmp = x; + x = y; + y = tmp; + } + sum = twosum(x, y); + hi = sum.hi; + lo = sum.lo; + } + if (lo !== 0) { + partials[actuallyUsedPartials] = lo; + actuallyUsedPartials += 1; + } + x = hi; + } + partials.length = actuallyUsedPartials; + if (x !== 0) partials[partials.length] = x; + } + + // compute the exact sum of partials, stopping once we lose precision + var n = partials.length - 1; + hi = 0; + lo = 0; + + if (overflow !== 0) { + var next = n >= 0 ? partials[n] : 0; + n -= 1; + if (abs(overflow) > 1 || (overflow > 0 && next > 0) || (overflow < 0 && next < 0)) { + return overflow > 0 ? $Infinity : -$Infinity; + } + // here we actually have to do the arithmetic + // drop a factor of 2 so we can do it without overflow + // assert(abs(overflow) === 1) + sum = twosum(overflow * POW_2_1023, next / 2); + hi = sum.hi; + lo = sum.lo; + lo *= 2; + if (abs(2 * hi) === $Infinity) { + // rounding to the maximum value + if (hi > 0) { + return (hi === POW_2_1023 && lo === -(MAX_ULP / 2) && n >= 0 && partials[n] < 0) ? MAX_DOUBLE : $Infinity; + } return (hi === -POW_2_1023 && lo === (MAX_ULP / 2) && n >= 0 && partials[n] > 0) ? -MAX_DOUBLE : -$Infinity; + } + + if (lo !== 0) { + partials[n + 1] = lo; + n += 1; + lo = 0; + } + + hi *= 2; + } + + while (n >= 0) { + x = hi; + y = partials[n]; + n -= 1; + sum = twosum(x, y); + hi = sum.hi; + lo = sum.lo; + if (lo !== 0) break; + } + + if (n >= 0 && ((lo < 0.0 && partials[n] < 0.0) || (lo > 0.0 && partials[n] > 0.0))) { + y = lo * 2.0; + x = hi + y; + if (y === x - hi) hi = x; + } + + return hi; + } +}); diff --git a/packages/core-js/proposals/math-sum.js b/packages/core-js/proposals/math-sum.js new file mode 100644 index 000000000000..bdd165d72d3f --- /dev/null +++ b/packages/core-js/proposals/math-sum.js @@ -0,0 +1,3 @@ +'use strict'; +// https://github.com/tc39/proposal-math-sum +require('../modules/esnext.math.sum-precise'); diff --git a/packages/core-js/stage/2.7.js b/packages/core-js/stage/2.7.js index eb63b47a92ae..e547bffb47bb 100644 --- a/packages/core-js/stage/2.7.js +++ b/packages/core-js/stage/2.7.js @@ -1,6 +1,7 @@ 'use strict'; var parent = require('./3'); +require('../proposals/math-sum'); require('../proposals/promise-try'); module.exports = parent; diff --git a/tests/compat/tests.js b/tests/compat/tests.js index 4fc6952b7545..22892c9a77c7 100644 --- a/tests/compat/tests.js +++ b/tests/compat/tests.js @@ -1767,6 +1767,9 @@ GLOBAL.tests = { 'esnext.math.signbit': function () { return Math.signbit; }, + 'esnext.math.sum-precise': function () { + return Math.sumPrecise; + }, 'esnext.number.from-string': function () { return Number.fromString; }, diff --git a/tests/entries/unit.mjs b/tests/entries/unit.mjs index 0931e76123a5..5c42ae13da0b 100644 --- a/tests/entries/unit.mjs +++ b/tests/entries/unit.mjs @@ -796,6 +796,7 @@ for (PATH of ['core-js-pure', 'core-js']) { ok(load(NS, 'math/scale')(3, 1, 2, 1, 2) === 3); ok(typeof load(NS, 'math/seeded-prng')({ seed: 42 }).next().value === 'number'); ok(load(NS, 'math/signbit')(-2) === true); + ok(load(NS, 'math/sum-precise')([1, 2, 3]) === 6); ok(load(NS, 'math/umulh')(0xFFFFFFFF, 7) === 6); ok(load(NS, 'map/of')([1, 2], [3, 4]) instanceof Map); ok(load(NS, 'map/reduce')(new Map([[1, 2], [2, 3], [3, 4]]), (a, b) => a + b) === 9); @@ -938,6 +939,7 @@ for (PATH of ['core-js-pure', 'core-js']) { load('proposals/map-upsert-stage-2'); load('proposals/math-extensions'); load('proposals/math-signbit'); + load('proposals/math-sum'); load('proposals/number-from-string'); load('proposals/number-range'); load('proposals/object-from-entries');