fix fesetenv(FE_DFL_ENV) on mips
[musl] / src / math / fma.c
index f44ecda..741ccd7 100644 (file)
@@ -1,3 +1,170 @@
+#include <fenv.h>
+#include "libm.h"
+
+#if LDBL_MANT_DIG==64 && LDBL_MAX_EXP==16384
+/* exact add, assumes exponent_x >= exponent_y */
+static void add(long double *hi, long double *lo, long double x, long double y)
+{
+       long double r;
+
+       r = x + y;
+       *hi = r;
+       r -= x;
+       *lo = y - r;
+}
+
+/* exact mul, assumes no over/underflow */
+static void mul(long double *hi, long double *lo, long double x, long double y)
+{
+       static const long double c = 1.0 + 0x1p32L;
+       long double cx, xh, xl, cy, yh, yl;
+
+       cx = c*x;
+       xh = (x - cx) + cx;
+       xl = x - xh;
+       cy = c*y;
+       yh = (y - cy) + cy;
+       yl = y - yh;
+       *hi = x*y;
+       *lo = (xh*yh - *hi) + xh*yl + xl*yh + xl*yl;
+}
+
+/*
+assume (long double)(hi+lo) == hi
+return an adjusted hi so that rounding it to double (or less) precision is correct
+*/
+static long double adjust(long double hi, long double lo)
+{
+       union ldshape uhi, ulo;
+
+       if (lo == 0)
+               return hi;
+       uhi.f = hi;
+       if (uhi.i.m & 0x3ff)
+               return hi;
+       ulo.f = lo;
+       if ((uhi.i.se & 0x8000) == (ulo.i.se & 0x8000))
+               uhi.i.m++;
+       else {
+               /* handle underflow and take care of ld80 implicit msb */
+               if (uhi.i.m << 1 == 0) {
+                       uhi.i.m = 0;
+                       uhi.i.se--;
+               }
+               uhi.i.m--;
+       }
+       return uhi.f;
+}
+
+/* adjusted add so the result is correct when rounded to double (or less) precision */
+static long double dadd(long double x, long double y)
+{
+       add(&x, &y, x, y);
+       return adjust(x, y);
+}
+
+/* adjusted mul so the result is correct when rounded to double (or less) precision */
+static long double dmul(long double x, long double y)
+{
+       mul(&x, &y, x, y);
+       return adjust(x, y);
+}
+
+static int getexp(long double x)
+{
+       union ldshape u;
+       u.f = x;
+       return u.i.se & 0x7fff;
+}
+
+double fma(double x, double y, double z)
+{
+       #pragma STDC FENV_ACCESS ON
+       long double hi, lo1, lo2, xy;
+       int round, ez, exy;
+
+       /* handle +-inf,nan */
+       if (!isfinite(x) || !isfinite(y))
+               return x*y + z;
+       if (!isfinite(z))
+               return z;
+       /* handle +-0 */
+       if (x == 0.0 || y == 0.0)
+               return x*y + z;
+       round = fegetround();
+       if (z == 0.0) {
+               if (round == FE_TONEAREST)
+                       return dmul(x, y);
+               return x*y;
+       }
+
+       /* exact mul and add require nearest rounding */
+       /* spurious inexact exceptions may be raised */
+       fesetround(FE_TONEAREST);
+       mul(&xy, &lo1, x, y);
+       exy = getexp(xy);
+       ez = getexp(z);
+       if (ez > exy) {
+               add(&hi, &lo2, z, xy);
+       } else if (ez > exy - 12) {
+               add(&hi, &lo2, xy, z);
+               if (hi == 0) {
+                       /*
+                       xy + z is 0, but it should be calculated with the
+                       original rounding mode so the sign is correct, if the
+                       compiler does not support FENV_ACCESS ON it does not
+                       know about the changed rounding mode and eliminates
+                       the xy + z below without the volatile memory access
+                       */
+                       volatile double z_;
+                       fesetround(round);
+                       z_ = z;
+                       return (xy + z_) + lo1;
+               }
+       } else {
+               /*
+               ez <= exy - 12
+               the 12 extra bits (1guard, 11round+sticky) are needed so with
+                       lo = dadd(lo1, lo2)
+               elo <= ehi - 11, and we use the last 10 bits in adjust so
+                       dadd(hi, lo)
+               gives correct result when rounded to double
+               */
+               hi = xy;
+               lo2 = z;
+       }
+       /*
+       the result is stored before return for correct precision and exceptions
+
+       one corner case is when the underflow flag should be raised because
+       the precise result is an inexact subnormal double, but the calculated
+       long double result is an exact subnormal double
+       (so rounding to double does not raise exceptions)
+
+       in nearest rounding mode dadd takes care of this: the last bit of the
+       result is adjusted so rounding sees an inexact value when it should
+
+       in non-nearest rounding mode fenv is used for the workaround
+       */
+       fesetround(round);
+       if (round == FE_TONEAREST)
+               z = dadd(hi, dadd(lo1, lo2));
+       else {
+#if defined(FE_INEXACT) && defined(FE_UNDERFLOW)
+               int e = fetestexcept(FE_INEXACT);
+               feclearexcept(FE_INEXACT);
+#endif
+               z = hi + (lo1 + lo2);
+#if defined(FE_INEXACT) && defined(FE_UNDERFLOW)
+               if (getexp(z) < 0x3fff-1022 && fetestexcept(FE_INEXACT))
+                       feraiseexcept(FE_UNDERFLOW);
+               else if (e)
+                       feraiseexcept(FE_INEXACT);
+#endif
+       }
+       return z;
+}
+#else
 /* origin: FreeBSD /usr/src/lib/msun/src/s_fma.c */
 /*-
  * Copyright (c) 2005-2011 David Schultz <das@FreeBSD.ORG>
  * SUCH DAMAGE.
  */
 
-#include <fenv.h>
-#include "libm.h"
-
 /*
  * A struct dd represents a floating-point number with twice the precision
  * of a double.  We maintain the invariant that "hi" stores the 53 high-order
@@ -68,16 +232,16 @@ static inline struct dd dd_add(double a, double b)
 static inline double add_adjusted(double a, double b)
 {
        struct dd sum;
-       uint64_t hibits, lobits;
+       union {double f; uint64_t i;} uhi, ulo;
 
        sum = dd_add(a, b);
        if (sum.lo != 0) {
-               EXTRACT_WORD64(hibits, sum.hi);
-               if ((hibits & 1) == 0) {
+               uhi.f = sum.hi;
+               if ((uhi.i & 1) == 0) {
                        /* hibits += (int)copysign(1.0, sum.hi * sum.lo) */
-                       EXTRACT_WORD64(lobits, sum.lo);
-                       hibits += 1 - ((hibits ^ lobits) >> 62);
-                       INSERT_WORD64(sum.hi, hibits);
+                       ulo.f = sum.lo;
+                       uhi.i += 1 - ((uhi.i ^ ulo.i) >> 62);
+                       sum.hi = uhi.f;
                }
        }
        return (sum.hi);
@@ -91,7 +255,7 @@ static inline double add_adjusted(double a, double b)
 static inline double add_and_denormalize(double a, double b, int scale)
 {
        struct dd sum;
-       uint64_t hibits, lobits;
+       union {double f; uint64_t i;} uhi, ulo;
        int bits_lost;
 
        sum = dd_add(a, b);
@@ -107,16 +271,16 @@ static inline double add_and_denormalize(double a, double b, int scale)
         * break the ties manually.
         */
        if (sum.lo != 0) {
-               EXTRACT_WORD64(hibits, sum.hi);
-               bits_lost = -((int)(hibits >> 52) & 0x7ff) - scale + 1;
-               if (bits_lost != 1 ^ (int)(hibits & 1)) {
+               uhi.f = sum.hi;
+               bits_lost = -((int)(uhi.i >> 52) & 0x7ff) - scale + 1;
+               if ((bits_lost != 1) ^ (int)(uhi.i & 1)) {
                        /* hibits += (int)copysign(1.0, sum.hi * sum.lo) */
-                       EXTRACT_WORD64(lobits, sum.lo);
-                       hibits += 1 - (((hibits ^ lobits) >> 62) & 2);
-                       INSERT_WORD64(sum.hi, hibits);
+                       ulo.f = sum.lo;
+                       uhi.i += 1 - (((uhi.i ^ ulo.i) >> 62) & 2);
+                       sum.hi = uhi.f;
                }
        }
-       return (ldexp(sum.hi, scale));
+       return scalbn(sum.hi, scale);
 }
 
 /*
@@ -167,6 +331,7 @@ static inline struct dd dd_mul(double a, double b)
  */
 double fma(double x, double y, double z)
 {
+       #pragma STDC FENV_ACCESS ON
        double xs, ys, zs, adj;
        struct dd xy, r;
        int oround;
@@ -178,14 +343,14 @@ double fma(double x, double y, double z)
         * return values here are crucial in handling special cases involving
         * infinities, NaNs, overflows, and signed zeroes correctly.
         */
-       if (x == 0.0 || y == 0.0)
-               return (x * y + z);
-       if (z == 0.0)
-               return (x * y);
        if (!isfinite(x) || !isfinite(y))
                return (x * y + z);
        if (!isfinite(z))
                return (z);
+       if (x == 0.0 || y == 0.0)
+               return (x * y + z);
+       if (z == 0.0)
+               return (x * y);
 
        xs = frexp(x, &ex);
        ys = frexp(y, &ey);
@@ -233,7 +398,7 @@ double fma(double x, double y, double z)
                }
        }
        if (spread <= DBL_MANT_DIG * 2)
-               zs = ldexp(zs, -spread);
+               zs = scalbn(zs, -spread);
        else
                zs = copysign(DBL_MIN, zs);
 
@@ -259,22 +424,37 @@ double fma(double x, double y, double z)
                 */
                fesetround(oround);
                volatile double vzs = zs; /* XXX gcc CSE bug workaround */
-               return (xy.hi + vzs + ldexp(xy.lo, spread));
+               return xy.hi + vzs + scalbn(xy.lo, spread);
        }
 
        if (oround != FE_TONEAREST) {
                /*
                 * There is no need to worry about double rounding in directed
                 * rounding modes.
+                * But underflow may not be raised properly, example in downward rounding:
+                * fma(0x1.000000001p-1000, 0x1.000000001p-30, -0x1p-1066)
                 */
+               double ret;
+#if defined(FE_INEXACT) && defined(FE_UNDERFLOW)
+               int e = fetestexcept(FE_INEXACT);
+               feclearexcept(FE_INEXACT);
+#endif
                fesetround(oround);
                adj = r.lo + xy.lo;
-               return (ldexp(r.hi + adj, spread));
+               ret = scalbn(r.hi + adj, spread);
+#if defined(FE_INEXACT) && defined(FE_UNDERFLOW)
+               if (ilogb(ret) < -1022 && fetestexcept(FE_INEXACT))
+                       feraiseexcept(FE_UNDERFLOW);
+               else if (e)
+                       feraiseexcept(FE_INEXACT);
+#endif
+               return ret;
        }
 
        adj = add_adjusted(r.lo, xy.lo);
        if (spread + ilogb(r.hi) > -1023)
-               return (ldexp(r.hi + adj, spread));
+               return scalbn(r.hi + adj, spread);
        else
-               return (add_and_denormalize(r.hi, adj, spread));
+               return add_and_denormalize(r.hi, adj, spread);
 }
+#endif