Skip to content
Merged
6 changes: 6 additions & 0 deletions Lib/test/test_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,12 @@ def testLdexp(self):
self.assertEqual(math.ldexp(NINF, n), NINF)
self.assertTrue(math.isnan(math.ldexp(NAN, n)))

@requires_IEEE_754
def testLdexp_denormal(self):
# Denormal output incorrectly rounded (truncated)
# on some Windows.
self.assertEqual(math.ldexp(6993274598585239, -1126), 1e-323)

def testLog(self):
self.assertRaises(TypeError, math.log)
self.assertRaises(TypeError, math.log, 1, 2, 3)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
``ldexp()`` on Windows doesn't round subnormal results before Windows 11,
but should. Python's :func:`math.ldexp` wrapper now does round them, so
results may change slightly, in rare cases of very small results, on
Windows versions before 11.
21 changes: 21 additions & 0 deletions Modules/mathmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2161,6 +2161,27 @@ math_ldexp_impl(PyObject *module, double x, PyObject *i)
} else {
errno = 0;
r = ldexp(x, (int)exp);
#ifdef _MSC_VER
if (DBL_MIN > r && r > -DBL_MIN) {
/* Denormal (or zero) results can be incorrectly rounded here (rather,
truncated). Fixed in newer versions of the C runtime, included
with Windows 11. */
int original_exp;
frexp(x, &original_exp);
if (original_exp > DBL_MIN_EXP) {
/* Shift down to the smallest normal binade. No bits lost. */
int shift = DBL_MIN_EXP - original_exp;
x = ldexp(x, shift);
exp -= shift;
}
/* Multiplying by 2**exp finishes the job, and the HW will round as
appropriate. Note: if exp < -DBL_MANT_DIG, all of x is shifted
to be < 0.5ULP of smallest denorm, so should be thrown away. If
exp is so very negative that ldexp underflows to 0, that's fine;
no need to check in advance. */
r = x*ldexp(1.0, (int)exp);
}
#endif
if (isinf(r))
errno = ERANGE;
}
Expand Down
Loading