소스 검색

Implement Math.sumPrecise (#1966)

Marko Lahma 10 달 전
부모
커밋
6b2d62286d
5개의 변경된 파일196개의 추가작업 그리고 68개의 파일을 삭제
  1. 0 1
      Jint.Tests.Test262/Test262Harness.settings.json
  2. 1 1
      Jint/Native/Math/MathInstance.cs
  3. 0 66
      Jint/Native/Math/MathX.cs
  4. 194 0
      Jint/Native/Math/SumPrecise.cs
  5. 1 0
      README.md

+ 0 - 1
Jint.Tests.Test262/Test262Harness.settings.json

@@ -11,7 +11,6 @@
     "decorators",
     "explicit-resource-management",
     "iterator-helpers",
-    "Math.sumPrecise",
     "regexp-lookbehind",
     "regexp-modifiers",
     "regexp-unicode-property-escapes",

+ 1 - 1
Jint/Native/Math/MathInstance.cs

@@ -1154,7 +1154,7 @@ internal sealed class MathInstance : ObjectInstance
             return state;
         }
 
-        return sum.FSum();
+        return Math.SumPrecise.Sum(sum);
     }
 
     private static double[] Coerced(JsValue[] arguments)

+ 0 - 66
Jint/Native/Math/MathX.cs

@@ -1,66 +0,0 @@
-// based on https://raw.githubusercontent.com/AnthonyLloyd/CsCheck/master/Tests/MathX.cs
-
-namespace Jint.Native.Math;
-
-internal static class MathX
-{
-    private static double TwoSum(double a, double b, out double lo)
-    {
-        var hi = a + b;
-        lo = hi - b;
-        lo = lo - hi + b + (a - lo);
-        return hi;
-    }
-
-    /// <summary>Shewchuk summation</summary>
-    internal static double FSum(this List<double> values)
-    {
-        if (values.Count < 3) return values.Count == 2 ? values[0] + values[1] : values.Count == 1 ? values[0] : 0.0;
-        Span<double> partials = stackalloc double[16];
-        var hi = TwoSum(values[0], values[1], out var lo);
-        int count = 0;
-        for (int i = 2; i < values.Count; i++)
-        {
-            var v = TwoSum(values[i], lo, out lo);
-            int c = 0;
-            for (int j = 0; j < count; j++)
-            {
-                v = TwoSum(v, partials[j], out var p);
-                if (p != 0.0)
-                    partials[c++] = p;
-            }
-
-            hi = TwoSum(hi, v, out v);
-            if (v != 0.0)
-            {
-                if (c == partials.Length)
-                {
-                    var newPartials = new double[partials.Length * 2];
-                    partials.CopyTo(newPartials);
-                    partials = newPartials;
-                }
-
-                partials[c++] = v;
-            }
-
-            count = c;
-        }
-
-        if (count != 0)
-        {
-            if (lo == 0) // lo has a good chance of being zero
-            {
-                lo = partials[0];
-                if (count == 1) return lo + hi;
-                partials = partials.Slice(1, count - 1);
-            }
-            else
-                partials = partials.Slice(0, count);
-
-            foreach (var p in partials)
-                lo += p;
-        }
-
-        return lo + hi;
-    }
-}

+ 194 - 0
Jint/Native/Math/SumPrecise.cs

@@ -0,0 +1,194 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Jint.Native.Math;
+
+/// <summary>
+/// https://raw.githubusercontent.com/es-shims/Math.sumPrecise/main/sum.js
+/// adapted from https://github.com/tc39/proposal-math-sum/blob/f4286d0a9d8525bda61be486df964bf2527c8789/polyfill/polyfill.mjs
+/// https://www-2.cs.cmu.edu/afs/cs/project/quake/public/papers/robust-arithmetic.ps
+/// Shewchuk's algorithm for exactly floating point addition
+/// as implemented in Python's fsum: https://github.com/python/cpython/blob/48dfd74a9db9d4aa9c6f23b4a67b461e5d977173/Modules/mathmodule.c#L1359-L1474
+/// adapted to handle overflow via an additional "biased" partial, representing 2**1024 times its actual value
+/// </summary>
+internal static class SumPrecise
+{
+    // exponent 11111111110, significand all 1s
+    private const double MaxDouble = 1.79769313486231570815e+308; // i.e. (2**1024 - 2**(1023 - 52))
+
+    // exponent 11111111110, significand all 1s except final 0
+    private const double PenultimateDouble = 1.79769313486231550856e+308; // i.e. (2**1024 - 2 * 2**(1023 - 52))
+
+    private const double Two1023 = 8.98846567431158e+307; // 2 ** 1023
+
+    // exponent 11111001010, significand all 0s
+    private const double MaxUlp = MaxDouble - PenultimateDouble; // 1.99584030953471981166e+292, i.e. 2**(1023 - 52)
+
+    [StructLayout(LayoutKind.Auto)]
+    private readonly record struct TwoSumResult(double Hi, double Lo);
+
+    // prerequisite: $abs(x) >= $abs(y)
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    private static TwoSumResult TwoSum(double x, double y)
+    {
+        var hi = x + y;
+        var lo = y - (hi - x);
+        return new TwoSumResult(hi, lo);
+    }
+
+    // preconditions:
+    //  - array only contains numbers
+    //  - none of them are -0, NaN, or ±Infinity
+    //  - all of them are finite
+    [MethodImpl(512)]
+    internal static double Sum(List<double> array)
+    {
+        double hi, lo;
+
+        List<double> partials = [];
+
+        var overflow = 0; // conceptually 2**1024 times this value; the final partial is biased by this amount
+
+        var index = -1;
+
+        // main loop
+        while (index + 1 < array.Count)
+        {
+            var x = +array[++index];
+
+            var actuallyUsedPartials = 0;
+            for (var j = 0; j < partials.Count; j += 1)
+            {
+                var y = partials[j];
+
+                if (System.Math.Abs(x) < System.Math.Abs(y))
+                {
+                    var tmp = x;
+                    x = y;
+                    y = tmp;
+                }
+
+                (hi, lo) = TwoSum(x, y);
+
+                if (double.IsPositiveInfinity(System.Math.Abs(hi)))
+                {
+                    var sign = double.IsPositiveInfinity(hi) ? 1 : -1;
+                    overflow += sign;
+
+                    x = x - sign * Two1023 - sign * Two1023;
+                    if (System.Math.Abs(x) < System.Math.Abs(y))
+                    {
+                        var tmp2 = x;
+                        x = y;
+                        y = tmp2;
+                    }
+
+                    var s2 = TwoSum(x, y);
+                    hi = s2.Hi;
+                    lo = s2.Lo;
+                }
+
+                if (lo != 0)
+                {
+                    partials[actuallyUsedPartials] = lo;
+                    actuallyUsedPartials += 1;
+                }
+
+                x = hi;
+            }
+
+            while (partials.Count > actuallyUsedPartials)
+            {
+                partials.RemoveAt(partials.Count - 1);
+            }
+
+            if (x != 0)
+            {
+                partials.Add(x);
+            }
+        }
+
+        // compute the exact sum of partials, stopping once we lose precision
+        var n = partials.Count - 1;
+        hi = 0;
+        lo = 0;
+
+        if (overflow != 0)
+        {
+            var next = n >= 0 ? partials[n] : 0;
+            n -= 1;
+            if (System.Math.Abs(overflow) > 1 || (overflow > 0 && next > 0) || (overflow < 0 && next < 0))
+            {
+                return overflow > 0 ? double.PositiveInfinity : double.NegativeInfinity;
+            }
+
+            // here we actually have to do the arithmetic
+            // drop a factor of 2 so we can do it without overflow
+            // assert($abs(overflow) === 1)
+            var s = TwoSum(overflow * Two1023, next / 2);
+            hi = s.Hi;
+            lo = s.Lo;
+            lo *= 2;
+            if (double.IsPositiveInfinity(System.Math.Abs(2 * hi)))
+            {
+                // stupid edge case: rounding to the maximum value
+                // MAX_DOUBLE has a 1 in the last place of its significand, so if we subtract exactly half a ULP from 2**1024, the result rounds away from it (i.e. to infinity) under ties-to-even
+                // but if the next partial has the opposite sign of the current value, we need to round towards MAX_DOUBLE instead
+                // this is the same as the "handle rounding" case below, but there's only one potentially-finite case we need to worry about, so we just hardcode that one
+                if (hi > 0)
+                {
+                    if (hi == Two1023 && lo == -(MaxUlp / 2) && n >= 0 && partials[n] < 0)
+                    {
+                        return MaxDouble;
+                    }
+
+                    return double.PositiveInfinity;
+                }
+
+                if (hi == -Two1023 && lo == (MaxUlp / 2) && n >= 0 && partials[n] > 0)
+                {
+                    return -MaxDouble;
+                }
+
+                return double.NegativeInfinity;
+            }
+
+            if (lo != 0)
+            {
+                partials[n + 1] = lo;
+                n += 1;
+                lo = 0;
+            }
+
+            hi *= 2;
+        }
+
+        while (n >= 0)
+        {
+            var x1 = hi;
+            var y1 = partials[n];
+            n -= 1;
+            // assert: $abs(x1) > $abs(y1)
+            (hi, lo) = TwoSum(x1, y1);
+            if (lo != 0)
+            {
+                break; // eslint-disable-line no-restricted-syntax
+            }
+        }
+
+        // handle rounding
+        // when the roundoff error is exactly half of the ULP for the result, we need to check one more partial to know which way to round
+        if (n >= 0 && ((lo < 0.0 && partials[n] < 0.0) || (lo > 0.0 && partials[n] > 0.0)))
+        {
+            var y2 = lo * 2.0;
+            var x2 = hi + y2;
+            var yr = x2 - hi;
+            if (y2 == yr)
+            {
+                hi = x2;
+            }
+        }
+
+        return hi;
+    }
+}

+ 1 - 0
README.md

@@ -133,6 +133,7 @@ and many more.
 - ✔ Float16Array (Requires NET 6 or higher)
 - ✔ Import attributes
 - ✔ JSON modules
+- ✔ Math.sumPrecise
 - ✔ `Promise.try`
 - ✔ Set methods (`intersection`, `union`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`)
 - ✔ ShadowRealm