https://github.com/python/cpython/commit/6157135a8d0bc2dbd5c24d1648d04a9c24a7d17e commit: 6157135a8d0bc2dbd5c24d1648d04a9c24a7d17e branch: main author: Sergey B Kirpichev <skirpic...@gmail.com> committer: vstinner <vstin...@python.org> date: 2025-04-28T15:23:26+02:00 summary:
gh-130317: Fix PyFloat_Pack/Unpack[24] for NaN's with payload (#130452) Co-authored-by: Victor Stinner <vstin...@python.org> files: A Misc/NEWS.d/next/Library/2025-02-22-13-07-06.gh-issue-130317.tnxd0I.rst M Lib/test/test_capi/test_float.py M Modules/_testcapi/clinic/float.c.h M Modules/_testcapi/float.c M Objects/floatobject.c diff --git a/Lib/test/test_capi/test_float.py b/Lib/test/test_capi/test_float.py index 92c987794142c9..c857959d569529 100644 --- a/Lib/test/test_capi/test_float.py +++ b/Lib/test/test_capi/test_float.py @@ -1,4 +1,5 @@ import math +import random import sys import unittest import warnings @@ -178,6 +179,39 @@ def test_pack_unpack_roundtrip(self): else: self.assertEqual(value2, value) + @unittest.skipUnless(HAVE_IEEE_754, "requires IEEE 754") + def test_pack_unpack_roundtrip_for_nans(self): + pack = _testcapi.float_pack + unpack = _testcapi.float_unpack + for _ in range(1000): + for size in (2, 4, 8): + sign = random.randint(0, 1) + signaling = random.randint(0, 1) + quiet = int(not signaling) + if size == 8: + payload = random.randint(signaling, 1 << 50) + i = (sign << 63) + (0x7ff << 52) + (quiet << 51) + payload + elif size == 4: + payload = random.randint(signaling, 1 << 21) + i = (sign << 31) + (0xff << 23) + (quiet << 22) + payload + elif size == 2: + payload = random.randint(signaling, 1 << 8) + i = (sign << 15) + (0x1f << 10) + (quiet << 9) + payload + data = bytes.fromhex(f'{i:x}') + for endian in (BIG_ENDIAN, LITTLE_ENDIAN): + with self.subTest(data=data, size=size, endian=endian): + data1 = data if endian == BIG_ENDIAN else data[::-1] + value = unpack(data1, endian) + if signaling and sys.platform == 'win32': + # On this platform sNaN becomes qNaN when returned + # from function. That's a known bug, e.g. + # https://developercommunity.visualstudio.com/t/155064 + # (see also gh-130317). + value = _testcapi.float_set_snan(value) + data2 = pack(size, value, endian) + self.assertTrue(math.isnan(value)) + self.assertEqual(data1, data2) + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Library/2025-02-22-13-07-06.gh-issue-130317.tnxd0I.rst b/Misc/NEWS.d/next/Library/2025-02-22-13-07-06.gh-issue-130317.tnxd0I.rst new file mode 100644 index 00000000000000..ab69f8806bfcb2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-02-22-13-07-06.gh-issue-130317.tnxd0I.rst @@ -0,0 +1,4 @@ +Fix :c:func:`PyFloat_Pack2` and :c:func:`PyFloat_Unpack2` for NaN's with +payload. This corrects round-trip for :func:`struct.unpack` and +:func:`struct.pack` in case of the IEEE 754 binary16 "half precision" type. +Patch by Sergey B Kirpichev. diff --git a/Modules/_testcapi/clinic/float.c.h b/Modules/_testcapi/clinic/float.c.h index d5a00c8072da1e..0710e4df1963cb 100644 --- a/Modules/_testcapi/clinic/float.c.h +++ b/Modules/_testcapi/clinic/float.c.h @@ -81,4 +81,13 @@ _testcapi_float_unpack(PyObject *module, PyObject *const *args, Py_ssize_t nargs exit: return return_value; } -/*[clinic end generated code: output=b43dfd3a77fe04ba input=a9049054013a1b77]*/ + +PyDoc_STRVAR(_testcapi_float_set_snan__doc__, +"float_set_snan($module, obj, /)\n" +"--\n" +"\n" +"Make a signaling NaN."); + +#define _TESTCAPI_FLOAT_SET_SNAN_METHODDEF \ + {"float_set_snan", (PyCFunction)_testcapi_float_set_snan, METH_O, _testcapi_float_set_snan__doc__}, +/*[clinic end generated code: output=1b0e9b05e1f50712 input=a9049054013a1b77]*/ diff --git a/Modules/_testcapi/float.c b/Modules/_testcapi/float.c index e3869134c84d43..2feeb205d8a376 100644 --- a/Modules/_testcapi/float.c +++ b/Modules/_testcapi/float.c @@ -157,9 +157,39 @@ test_string_to_double(PyObject *self, PyObject *Py_UNUSED(ignored)) } +/*[clinic input] +_testcapi.float_set_snan + + obj: object + / + +Make a signaling NaN. +[clinic start generated code]*/ + +static PyObject * +_testcapi_float_set_snan(PyObject *module, PyObject *obj) +/*[clinic end generated code: output=f43778a70f60aa4b input=c1269b0f88ef27ac]*/ +{ + if (!PyFloat_Check(obj)) { + PyErr_SetString(PyExc_ValueError, "float-point number expected"); + return NULL; + } + double d = ((PyFloatObject *)obj)->ob_fval; + if (!isnan(d)) { + PyErr_SetString(PyExc_ValueError, "nan expected"); + return NULL; + } + uint64_t v; + memcpy(&v, &d, 8); + v &= ~(1ULL << 51); /* make sNaN */ + memcpy(&d, &v, 8); + return PyFloat_FromDouble(d); +} + static PyMethodDef test_methods[] = { _TESTCAPI_FLOAT_PACK_METHODDEF _TESTCAPI_FLOAT_UNPACK_METHODDEF + _TESTCAPI_FLOAT_SET_SNAN_METHODDEF {"test_string_to_double", test_string_to_double, METH_NOARGS}, {NULL}, }; diff --git a/Objects/floatobject.c b/Objects/floatobject.c index 87a00bf1a458ea..e0a8f0c62d4951 100644 --- a/Objects/floatobject.c +++ b/Objects/floatobject.c @@ -2021,14 +2021,13 @@ PyFloat_Pack2(double x, char *data, int le) bits = 0; } else if (isnan(x)) { - /* There are 2046 distinct half-precision NaNs (1022 signaling and - 1024 quiet), but there are only two quiet NaNs that don't arise by - quieting a signaling NaN; we get those by setting the topmost bit - of the fraction field and clearing all other fraction bits. We - choose the one with the appropriate sign. */ sign = (copysign(1.0, x) == -1.0); e = 0x1f; - bits = 512; + + uint64_t v; + memcpy(&v, &x, sizeof(v)); + v &= 0xffc0000000000ULL; + bits = (unsigned short)(v >> 42); /* NaN's type & payload */ } else { sign = (x < 0.0); @@ -2192,6 +2191,21 @@ PyFloat_Pack4(double x, char *data, int le) if (isinf(y) && !isinf(x)) goto Overflow; + /* correct y if x was a sNaN, transformed to qNaN by conversion */ + if (isnan(x)) { + uint64_t v; + + memcpy(&v, &x, 8); + if ((v & (1ULL << 51)) == 0) { + union float_val { + float f; + uint32_t u32; + } *py = (union float_val *)&y; + + py->u32 &= ~(1 << 22); /* make sNaN */ + } + } + unsigned char s[sizeof(float)]; memcpy(s, &y, sizeof(float)); @@ -2374,7 +2388,11 @@ PyFloat_Unpack2(const char *data, int le) } else { /* NaN */ - return sign ? -fabs(Py_NAN) : fabs(Py_NAN); + uint64_t v = sign ? 0xfff0000000000000ULL : 0x7ff0000000000000ULL; + + v += (uint64_t)f << 42; /* add NaN's type & payload */ + memcpy(&x, &v, sizeof(v)); + return x; } } @@ -2470,6 +2488,23 @@ PyFloat_Unpack4(const char *data, int le) memcpy(&x, p, 4); } + /* return sNaN double if x was sNaN float */ + if (isnan(x)) { + uint32_t v; + memcpy(&v, &x, 4); + + if ((v & (1 << 22)) == 0) { + double y = x; /* will make qNaN double */ + union double_val { + double d; + uint64_t u64; + } *py = (union double_val *)&y; + + py->u64 &= ~(1ULL << 51); /* make sNaN */ + return y; + } + } + return x; } } _______________________________________________ Python-checkins mailing list -- python-checkins@python.org To unsubscribe send an email to python-checkins-le...@python.org https://mail.python.org/mailman3/lists/python-checkins.python.org/ Member address: arch...@mail-archive.com