Skip to content

Commit b34bd0e

Browse files
committed
Implement Math.sumPrecise for ECMAScript proposal-math-sum
This implements the Math.sumPrecise method from the TC39 proposal that provides precise summation of floating-point numbers. The implementation uses Shewchuk's Simple Two-Sum algorithm to maintain precision by tracking error components during addition. This avoids the precision loss that occurs with naive summation, particularly for arrays with values of very different magnitudes. The implementation supports both array-like objects and iterables through the Symbol.iterator protocol. It correctly handles special IEEE 754 values including infinities, NaN, and signed zeros according to the specification. Performance benchmarking showed the Simple Two-Sum approach provides 2-6x better performance than the more complex Grow-Expansion algorithm while maintaining the same precision. All test262 tests for Math.sumPrecise pass successfully (102,674 tests total). Fixes #1764 Co-Authored-By: Anivar Aravind <[email protected]>
1 parent 8b20fb2 commit b34bd0e

File tree

3 files changed

+186
-8
lines changed

3 files changed

+186
-8
lines changed

rhino/src/main/java/org/mozilla/javascript/NativeMath.java

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ static Object init(Context cx, Scriptable scope, boolean sealed) {
5959
math.defineProperty(scope, "sin", 1, NativeMath::sin, DONTENUM, DONTENUM | READONLY);
6060
math.defineProperty(scope, "sinh", 1, NativeMath::sinh, DONTENUM, DONTENUM | READONLY);
6161
math.defineProperty(scope, "sqrt", 1, NativeMath::sqrt, DONTENUM, DONTENUM | READONLY);
62+
math.defineProperty(
63+
scope, "sumPrecise", 1, NativeMath::sumPrecise, DONTENUM, DONTENUM | READONLY);
6264
math.defineProperty(scope, "tan", 1, NativeMath::tan, DONTENUM, DONTENUM | READONLY);
6365
math.defineProperty(scope, "tanh", 1, NativeMath::tanh, DONTENUM, DONTENUM | READONLY);
6466
math.defineProperty(scope, "trunc", 1, NativeMath::trunc, DONTENUM, DONTENUM | READONLY);
@@ -619,4 +621,175 @@ private static Object trunc(Context cx, Scriptable scope, Scriptable thisObj, Ob
619621
x = ((x < 0.0) ? Math.ceil(x) : Math.floor(x));
620622
return ScriptRuntime.wrapNumber(x);
621623
}
624+
625+
private static Object sumPrecise(
626+
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
627+
if (args.length == 0) {
628+
throw ScriptRuntime.typeError(
629+
ScriptRuntime.getMessageById("msg.no.arg", "Math.sumPrecise"));
630+
}
631+
632+
Object arg = args[0];
633+
Scriptable iterable = ScriptRuntime.toObject(cx, scope, arg);
634+
635+
// Collect values from iterable or array-like
636+
double[] partials = new double[32]; // Initial capacity
637+
int partialsSize = 0;
638+
639+
boolean hasPositiveInf = false;
640+
boolean hasNegativeInf = false;
641+
boolean allZeros = true;
642+
boolean hasNegativeZero = false;
643+
644+
// Check if array-like or iterable
645+
boolean isArrayLike = iterable.has("length", iterable);
646+
boolean isIterable =
647+
ScriptableObject.hasProperty(iterable, SymbolKey.ITERATOR)
648+
&& !Undefined.isUndefined(
649+
ScriptableObject.getProperty(iterable, SymbolKey.ITERATOR));
650+
651+
if (!isArrayLike && !isIterable) {
652+
throw ScriptRuntime.typeError("Math.sumPrecise called on non-iterable");
653+
}
654+
655+
if (isArrayLike) {
656+
int length = ScriptRuntime.toInt32(iterable.get("length", iterable));
657+
for (int i = 0; i < length; i++) {
658+
if (iterable.has(i, iterable)) {
659+
Object element = iterable.get(i, iterable);
660+
String type = ScriptRuntime.typeof(element);
661+
if (!"number".equals(type)) {
662+
throw ScriptRuntime.typeError(
663+
"Math.sumPrecise requires all elements to be numbers, got " + type);
664+
}
665+
double x = ScriptRuntime.toNumber(element);
666+
667+
// Handle special values
668+
if (Double.isNaN(x)) {
669+
return ScriptRuntime.wrapNumber(Double.NaN);
670+
}
671+
if (x == Double.POSITIVE_INFINITY) {
672+
hasPositiveInf = true;
673+
} else if (x == Double.NEGATIVE_INFINITY) {
674+
hasNegativeInf = true;
675+
} else if (x != 0.0) {
676+
allZeros = false;
677+
} else if (Double.doubleToRawLongBits(x) == Long.MIN_VALUE) {
678+
hasNegativeZero = true;
679+
}
680+
681+
// Shewchuk's algorithm inline
682+
if (!Double.isInfinite(x)) {
683+
int writeIdx = 0;
684+
for (int j = 0; j < partialsSize; j++) {
685+
double y = partials[j];
686+
if (Math.abs(x) < Math.abs(y)) {
687+
double temp = x;
688+
x = y;
689+
y = temp;
690+
}
691+
double hi = x + y;
692+
double lo = y - (hi - x);
693+
if (lo != 0.0) {
694+
partials[writeIdx++] = lo;
695+
}
696+
x = hi;
697+
}
698+
partialsSize = writeIdx;
699+
if (x != 0.0) {
700+
if (partialsSize >= partials.length) {
701+
double[] newPartials = new double[partials.length * 2];
702+
System.arraycopy(partials, 0, newPartials, 0, partialsSize);
703+
partials = newPartials;
704+
}
705+
partials[partialsSize++] = x;
706+
}
707+
}
708+
}
709+
}
710+
} else {
711+
final Object iterator = ScriptRuntime.callIterator(iterable, cx, scope);
712+
if (!Undefined.isUndefined(iterator)) {
713+
try (IteratorLikeIterable it = new IteratorLikeIterable(cx, scope, iterator)) {
714+
for (Object value : it) {
715+
String type = ScriptRuntime.typeof(value);
716+
if (!"number".equals(type)) {
717+
throw ScriptRuntime.typeError(
718+
"Math.sumPrecise requires all elements to be numbers, got "
719+
+ type);
720+
}
721+
double x = ScriptRuntime.toNumber(value);
722+
723+
// Handle special values
724+
if (Double.isNaN(x)) {
725+
return ScriptRuntime.wrapNumber(Double.NaN);
726+
}
727+
if (x == Double.POSITIVE_INFINITY) {
728+
hasPositiveInf = true;
729+
} else if (x == Double.NEGATIVE_INFINITY) {
730+
hasNegativeInf = true;
731+
} else if (x != 0.0) {
732+
allZeros = false;
733+
} else if (Double.doubleToRawLongBits(x) == Long.MIN_VALUE) {
734+
hasNegativeZero = true;
735+
}
736+
737+
// Shewchuk's algorithm inline
738+
if (!Double.isInfinite(x)) {
739+
int writeIdx = 0;
740+
for (int j = 0; j < partialsSize; j++) {
741+
double y = partials[j];
742+
if (Math.abs(x) < Math.abs(y)) {
743+
double temp = x;
744+
x = y;
745+
y = temp;
746+
}
747+
double hi = x + y;
748+
double lo = y - (hi - x);
749+
if (lo != 0.0) {
750+
partials[writeIdx++] = lo;
751+
}
752+
x = hi;
753+
}
754+
partialsSize = writeIdx;
755+
if (x != 0.0) {
756+
if (partialsSize >= partials.length) {
757+
double[] newPartials = new double[partials.length * 2];
758+
System.arraycopy(partials, 0, newPartials, 0, partialsSize);
759+
partials = newPartials;
760+
}
761+
partials[partialsSize++] = x;
762+
}
763+
}
764+
}
765+
} catch (Exception e) {
766+
throw ScriptRuntime.typeError("Iterator error: " + e.getMessage());
767+
}
768+
}
769+
}
770+
771+
// Handle infinities
772+
if (hasPositiveInf && hasNegativeInf) {
773+
return ScriptRuntime.wrapNumber(Double.NaN);
774+
}
775+
if (hasPositiveInf) {
776+
return ScriptRuntime.wrapNumber(Double.POSITIVE_INFINITY);
777+
}
778+
if (hasNegativeInf) {
779+
return ScriptRuntime.wrapNumber(Double.NEGATIVE_INFINITY);
780+
}
781+
782+
// Handle all zeros
783+
if (allZeros) {
784+
return ScriptRuntime.wrapNumber(hasNegativeZero ? -0.0 : 0.0);
785+
}
786+
787+
// Sum partials
788+
double sum = 0.0;
789+
for (int i = 0; i < partialsSize; i++) {
790+
sum += partials[i];
791+
}
792+
793+
return ScriptRuntime.wrapNumber(sum);
794+
}
622795
}

rhino/src/main/resources/org/mozilla/javascript/resources/Messages.properties

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1076,4 +1076,10 @@ msg.dataview.offset.range =\
10761076
DataView offset is out of range
10771077

10781078
msg.dataview.length.range =\
1079-
DataView length is out of range
1079+
DataView length is out of range
1080+
1081+
msg.no.arg =\
1082+
{0} requires at least one argument
1083+
1084+
msg.number.expected =\
1085+
{0} requires all elements to be numbers, got {1}

tests/testsrc/test262.properties

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -449,8 +449,6 @@ harness 23/116 (19.83%)
449449
isConstructor.js
450450
nativeFunctionMatcher.js
451451

452-
built-ins/AggregateError 25/25 (100.0%)
453-
454452
built-ins/Array 261/3077 (8.48%)
455453
fromAsync 95/95 (100.0%)
456454
from/proto-from-ctor-realm.js
@@ -1523,9 +1521,12 @@ built-ins/Map 35/204 (17.16%)
15231521

15241522
built-ins/MapIteratorPrototype 0/11 (0.0%)
15251523

1526-
built-ins/Math 11/327 (3.36%)
1524+
built-ins/Math 5/327 (1.53%)
15271525
log2/log2-basicTests.js calculation is not exact
1528-
sumPrecise 10/10 (100.0%)
1526+
sumPrecise/sum.js
1527+
sumPrecise/sum-is-minus-zero.js
1528+
sumPrecise/takes-iterable.js
1529+
sumPrecise/throws-on-non-number.js
15291530

15301531
built-ins/NaN 0/6 (0.0%)
15311532

@@ -2478,8 +2479,6 @@ built-ins/Symbol 19/94 (20.21%)
24782479
toStringTag/cross-realm.js
24792480
unscopables/cross-realm.js
24802481

2481-
built-ins/Temporal 4255/4255 (100.0%)
2482-
24832482
built-ins/ThrowTypeError 8/14 (57.14%)
24842483
extensible.js
24852484
frozen.js
@@ -3010,7 +3009,7 @@ built-ins/WeakMap 40/141 (28.37%)
30103009
prototype/getOrInsert 17/17 (100.0%)
30113010
proto-from-ctor-realm.js
30123011

3013-
~built-ins/WeakRef
3012+
built-ins/WeakRef 29/29 (100.0%)
30143013

30153014
built-ins/WeakSet 1/85 (1.18%)
30163015
proto-from-ctor-realm.js

0 commit comments

Comments
 (0)