diff --git a/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Libraries/LibJS/Runtime/CommonPropertyNames.h index 9962d778a4e..229539f4c98 100644 --- a/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -499,6 +499,7 @@ namespace JS { P(substr) \ P(substring) \ P(subtract) \ + P(sumPrecise) \ P(sup) \ P(supportedLocalesOf) \ P(supportedValuesOf) \ diff --git a/Libraries/LibJS/Runtime/ErrorTypes.h b/Libraries/LibJS/Runtime/ErrorTypes.h index be99309b761..206e4772ca0 100644 --- a/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Libraries/LibJS/Runtime/ErrorTypes.h @@ -95,6 +95,7 @@ M(JsonBigInt, "Cannot serialize BigInt value to JSON") \ M(JsonCircular, "Cannot stringify circular object") \ M(JsonMalformed, "Malformed JSON string") \ + M(MathSumPreciseOverflow, "Overflow in Math.sumPrecise") \ M(MissingRequiredProperty, "Required property {} is missing or undefined") \ M(ModuleNoEnvironment, "Cannot find module environment for imported binding") \ M(ModuleNotFound, "Cannot find/open module: '{}'") \ diff --git a/Libraries/LibJS/Runtime/MathObject.cpp b/Libraries/LibJS/Runtime/MathObject.cpp index dc671cd80db..715b49655b3 100644 --- a/Libraries/LibJS/Runtime/MathObject.cpp +++ b/Libraries/LibJS/Runtime/MathObject.cpp @@ -10,7 +10,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -65,6 +67,7 @@ void MathObject::initialize(Realm& realm) define_native_function(realm, vm.names.sinh, sinh, 1, attr); define_native_function(realm, vm.names.cosh, cosh, 1, attr); define_native_function(realm, vm.names.tanh, tanh, 1, attr); + define_native_function(realm, vm.names.sumPrecise, sumPrecise, 1, attr); // 21.3.1 Value Properties of the Math Object, https://tc39.es/ecma262/#sec-value-properties-of-the-math-object define_direct_property(vm.names.E, Value(M_E), 0); @@ -957,4 +960,241 @@ JS_DEFINE_NATIVE_FUNCTION(MathObject::trunc) : ::floor(number.as_double())); } +struct TwoSumResult { + double hi; + double lo; +}; + +static TwoSumResult two_sum(double x, double y) +{ + double hi = x + y; + double lo = y - (hi - x); + return { hi, lo }; +} + +// https://tc39.es/proposal-math-sum/#sec-math.sumprecise +ThrowCompletionOr MathObject::sum_precise_impl(VM& vm, Value iterable) +{ + constexpr double MAX_DOUBLE = 1.79769313486231570815e+308; // std::numeric_limits::max() + constexpr double PENULTIMATE_DOUBLE = 1.79769313486231550856e+308; // std::nextafter(DBL_MAX, 0) + constexpr double MAX_ULP = MAX_DOUBLE - PENULTIMATE_DOUBLE; + constexpr double POW_2_1023 = 8.98846567431158e+307; // 2^1023 + + // 1. Perform ? RequireObjectCoercible(items). + TRY(require_object_coercible(vm, iterable)); + + // 2. Let iteratorRecord be ? GetIterator(items, sync). + auto using_iterator = TRY(iterable.get_method(vm, vm.well_known_symbol_iterator())); + if (!using_iterator) + return vm.throw_completion(ErrorType::NotIterable, iterable.to_string_without_side_effects()); + + auto iterator = TRY(get_iterator_from_method(vm, iterable, *using_iterator)); + + enum State { + MinusZero, + PlusInfinity, + MinusInfinity, + NotANumber, + Finite + }; + + // 3. Let state be minus-zero. + State state = State::MinusZero; + + // 4. Let sum be 0. + // 5. Let count be 0. + double overflow = 0.0; + u64 count = 0; + Vector partials; + + // 6. Let next be not-started. + // 7. Repeat, while next is not done + for (;;) { + // a. Set next to ? IteratorStepValue(iteratorRecord). + auto next = TRY(iterator_step_value(vm, iterator)); + + if (!next.has_value()) + break; + + // If next is not done, then + // i. Set count to count + 1. + count++; + // ii. If count ≥ 2**53, then + // 1. Let error be ThrowCompletion(a newly created RangeError object). + // 2. Return ? IteratorClose(iteratorRecord, error). + if (count >= (1ULL << 53)) + return iterator_close(vm, iterator, vm.throw_completion(ErrorType::ArrayMaxSize)); + + // iii. NOTE: The above case is not expected to be reached in practice and is included only so that implementations may rely on inputs being + // "reasonably sized" without violating this specification. + + // iv. If next is not a Number, then + auto value = next.value(); + if (!value.is_number()) + // 1. Let error be ThrowCompletion(a newly created TypeError object). + // 2. Return ? IteratorClose(iteratorRecord, error). + return iterator_close(vm, iterator, vm.throw_completion(ErrorType::IsNotA, value.to_string_without_side_effects(), "number")); + + // v. Let n be next. + auto n = value.as_double(); + + // vi. If state is not not-a-number, then + if (state != State::NotANumber) { + // 1. If n is NaN, then + if (isnan(n)) { + // a. Set state to not-a-number. + state = State::NotANumber; + } // 2. Else if n is +∞𝔽, then + else if (Value(n).is_positive_infinity()) { + // a. If state is minus-infinity, set state to not-a-number. + // b. Else, set state to plus-infinity. + state = (state == State::MinusInfinity) ? State::NotANumber : State::PlusInfinity; + } // 3. Else if n is -∞𝔽, then + else if (Value(n).is_negative_infinity()) { + // a. If state is plus-infinity, set state to not-a-number. + // b. Else, set state to minus-infinity. + state = (state == State::PlusInfinity) ? State::NotANumber : State::MinusInfinity; + } // 4. Else if n is not -0𝔽 and state is either minus-zero or finite, then + else if (!Value(n).is_negative_zero() && (state == State::MinusZero || state == State::Finite)) { + // a. Set state to finite. + state = State::Finite; + + // b. Set sum to sum + ℝ(n). + double x = n; + size_t used_partials = 0; + + for (size_t i = 0; i < partials.size(); i++) { + double y = partials[i]; + + if (AK::abs(x) < AK::abs(y)) + swap(x, y); + + TwoSumResult result = two_sum(x, y); + double hi = result.hi; + double lo = result.lo; + + if (isinf(hi)) { + double sign = signbit(hi) ? -1.0 : 1.0; + overflow += sign; + + if (AK::abs(overflow) >= (1ULL << 53)) + return vm.throw_completion(ErrorType::MathSumPreciseOverflow); + + x = (x - sign * POW_2_1023) - sign * POW_2_1023; + + if (AK::abs(x) < AK::abs(y)) + swap(x, y); + + result = two_sum(x, y); + hi = result.hi; + lo = result.lo; + } + + if (lo != 0.0) { + partials[used_partials++] = lo; + } + + x = hi; + } + + partials.resize(used_partials); + + if (x != 0.0) { + partials.append(x); + } + } + } + } + + // 8. If state is not-a-number, return NaN. + if (state == State::NotANumber) + return js_nan(); + + // 9. If state is plus-infinity, return +∞𝔽. + if (state == State::PlusInfinity) + return js_infinity(); + + // 10. If state is minus-infinity, return -∞𝔽. + if (state == State::MinusInfinity) + return js_negative_infinity(); + + // 11. If state is minus-zero, return -0𝔽. + if (state == State::MinusZero) + return Value(-0.0); + + // 12. Return 𝔽(sum). + int n = partials.size() - 1; + double hi = 0.0; + double lo = 0.0; + + if (overflow != 0.0) { + double next = n >= 0 ? partials[n] : 0.0; + n--; + + if (AK::abs(overflow) > 1.0 || (overflow > 0.0 && next > 0.0) || (overflow < 0.0 && next < 0.0)) { + return overflow > 0.0 ? js_infinity() : js_negative_infinity(); + } + + TwoSumResult result = two_sum(overflow * POW_2_1023, next / 2.0); + hi = result.hi; + lo = result.lo * 2.0; + + if (isinf(hi * 2.0)) { + if (hi > 0.0) { + if (hi == POW_2_1023 && lo == -(MAX_ULP / 2.0) && n >= 0 && partials[n] < 0.0) { + return Value(MAX_DOUBLE); + } + + return js_infinity(); + } else { + if (hi == -POW_2_1023 && lo == (MAX_ULP / 2.0) && n >= 0 && partials[n] > 0.0) { + return Value(-MAX_DOUBLE); + } + + return js_negative_infinity(); + } + } + + if (lo != 0.0) { + partials[n + 1] = lo; + n++; + lo = 0.0; + } + + hi *= 2.0; + } + + while (n >= 0) { + double x = hi; + double y = partials[n]; + n--; + + TwoSumResult result = two_sum(x, y); + hi = result.hi; + lo = result.lo; + + if (lo != 0.0) { + break; + } + } + + if (n >= 0 && ((lo < 0.0 && partials[n] < 0.0) || (lo > 0.0 && partials[n] > 0.0))) { + double y = lo * 2.0; + double x = hi + y; + double yr = x - hi; + + if (y == yr) { + hi = x; + } + } + + return Value(hi); +} + +// https://tc39.es/proposal-math-sum/#sec-math.sumprecise +JS_DEFINE_NATIVE_FUNCTION(MathObject::sumPrecise) +{ + return sum_precise_impl(vm, vm.argument(0)); +} + } diff --git a/Libraries/LibJS/Runtime/MathObject.h b/Libraries/LibJS/Runtime/MathObject.h index c5705faf770..12f415f2031 100644 --- a/Libraries/LibJS/Runtime/MathObject.h +++ b/Libraries/LibJS/Runtime/MathObject.h @@ -26,6 +26,7 @@ public: static ThrowCompletionOr round_impl(VM&, Value); static ThrowCompletionOr exp_impl(VM&, Value); static ThrowCompletionOr abs_impl(VM&, Value); + static ThrowCompletionOr sum_precise_impl(VM&, Value); private: explicit MathObject(Realm&); @@ -66,6 +67,7 @@ private: JS_DECLARE_NATIVE_FUNCTION(sinh); JS_DECLARE_NATIVE_FUNCTION(cosh); JS_DECLARE_NATIVE_FUNCTION(tanh); + JS_DECLARE_NATIVE_FUNCTION(sumPrecise); }; } diff --git a/Libraries/LibJS/Tests/builtins/Math/Math.sumPrecise.js b/Libraries/LibJS/Tests/builtins/Math/Math.sumPrecise.js new file mode 100644 index 00000000000..464cc7f0d2e --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Math/Math.sumPrecise.js @@ -0,0 +1,97 @@ +test("basic functionality", () => { + expect(Math.sumPrecise).toHaveLength(1); + + expect(Math.sumPrecise([])).toBe(-0); + expect(Math.sumPrecise([1, 2, 3])).toBe(6); + expect(Math.sumPrecise([1e308])).toBe(1e308); + expect(Math.sumPrecise([1e308, -1e308])).toBe(0); + expect(Math.sumPrecise([0.1])).toBe(0.1); + expect(Math.sumPrecise([0.1, 0.1])).toBe(0.2); + expect(Math.sumPrecise([0.1, -0.1])).toBe(0); + expect(Math.sumPrecise([1e308, 1e308, 0.1, 0.1, 1e30, 0.1, -1e30, -1e308, -1e308])).toBe( + 0.30000000000000004 + ); + expect(Math.sumPrecise([1e30, 0.1, -1e30])).toBe(0.1); + + expect( + Math.sumPrecise([8.98846567431158e307, 8.988465674311579e307, -1.7976931348623157e308]) + ).toBe(9.9792015476736e291); + expect( + Math.sumPrecise([-5.630637621603525e255, 9.565271205476345e307, 2.9937604643020797e292]) + ).toBe(9.565271205476347e307); + expect( + Math.sumPrecise([ + 6.739986666787661e66, 2, -1.2689709186578243e-116, 1.7046015739467354e308, + -9.979201547673601e291, 6.160926733208294e307, -3.179557053031852e234, + -7.027282978772846e307, -0.7500000000000001, + ]) + ).toBe(1.61796594939028e308); + expect( + Math.sumPrecise([ + 0.31150493246968836, -8.988465674311582e307, 1.8315037361673755e-270, + -15.999999999999996, 2.9999999999999996, 7.345200721499384e164, -2.033582473639399, + -8.98846567431158e307, -3.5737295155405993e292, 4.13894772383715e-124, + -3.6111186457260667e-35, 2.387234887098013e180, 7.645295562778372e-298, + 3.395189016861822e-103, -2.6331611115768973e-149, + ]) + ).toBe(-Infinity); + expect( + Math.sumPrecise([ + -1.1442589134409902e308, 9.593842098384855e138, 4.494232837155791e307, + -1.3482698511467367e308, 4.494232837155792e307, + ]) + ).toBe(-1.5936821971565685e308); + expect( + Math.sumPrecise([ + -1.1442589134409902e308, 4.494232837155791e307, -1.3482698511467367e308, + 4.494232837155792e307, + ]) + ).toBe(-1.5936821971565687e308); + expect( + Math.sumPrecise([ + 9.593842098384855e138, -6.948356297254111e307, -1.3482698511467367e308, + 4.494232837155792e307, + ]) + ).toBe(-1.5936821971565685e308); + expect( + Math.sumPrecise([-2.534858246857893e115, 8.988465674311579e307, 8.98846567431158e307]), + 1.7976931348623157e308 + ); + expect( + Math.sumPrecise([1.3588124894186193e308, 1.4803986201152006e223, 6.741349255733684e307]) + ).toBe(Infinity); + expect( + Math.sumPrecise([6.741349255733684e307, 1.7976931348623155e308, -7.388327292663961e41]) + ).toBe(Infinity); + expect( + Math.sumPrecise([-1.9807040628566093e28, 1.7976931348623157e308, 9.9792015476736e291]) + ).toBe(1.7976931348623157e308); + expect( + Math.sumPrecise([ + -1.0214557991173964e61, 1.7976931348623157e308, 8.98846567431158e307, + -8.988465674311579e307, + ]) + ).toBe(1.7976931348623157e308); + expect( + Math.sumPrecise([ + 1.7976931348623157e308, 7.999999999999999, -1.908963895403937e-230, + 1.6445950082320264e292, 2.0734856707605806e205, + ]) + ).toBe(Infinity); + expect( + Math.sumPrecise([6.197409167220438e-223, -9.979201547673601e291, -1.7976931348623157e308]) + ).toBe(-Infinity); + expect( + Math.sumPrecise([ + 4.49423283715579e307, 8.944251746776101e307, -0.0002441406250000001, + 1.1752060710043817e308, 4.940846717201632e292, -1.6836699406454528e308, + ]) + ).toBe(8.353845887521184e307); + expect( + Math.sumPrecise([ + 8.988465674311579e307, 7.999999999999998, 7.029158107234023e-308, + -2.2303483759420562e-172, -1.7976931348623157e308, -8.98846567431158e307, + ]) + ).toBe(-1.7976931348623157e308); + expect(Math.sumPrecise([8.98846567431158e307, 8.98846567431158e307])).toBe(Infinity); +});