Skip to content

Commit 6157135

Browse files
skirpichevvstinner
andauthored
gh-130317: Fix PyFloat_Pack/Unpack[24] for NaN's with payload (#130452)
Co-authored-by: Victor Stinner <[email protected]>
1 parent 922049b commit 6157135

File tree

5 files changed

+120
-8
lines changed

5 files changed

+120
-8
lines changed

Lib/test/test_capi/test_float.py

+34
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import math
2+
import random
23
import sys
34
import unittest
45
import warnings
@@ -178,6 +179,39 @@ def test_pack_unpack_roundtrip(self):
178179
else:
179180
self.assertEqual(value2, value)
180181

182+
@unittest.skipUnless(HAVE_IEEE_754, "requires IEEE 754")
183+
def test_pack_unpack_roundtrip_for_nans(self):
184+
pack = _testcapi.float_pack
185+
unpack = _testcapi.float_unpack
186+
for _ in range(1000):
187+
for size in (2, 4, 8):
188+
sign = random.randint(0, 1)
189+
signaling = random.randint(0, 1)
190+
quiet = int(not signaling)
191+
if size == 8:
192+
payload = random.randint(signaling, 1 << 50)
193+
i = (sign << 63) + (0x7ff << 52) + (quiet << 51) + payload
194+
elif size == 4:
195+
payload = random.randint(signaling, 1 << 21)
196+
i = (sign << 31) + (0xff << 23) + (quiet << 22) + payload
197+
elif size == 2:
198+
payload = random.randint(signaling, 1 << 8)
199+
i = (sign << 15) + (0x1f << 10) + (quiet << 9) + payload
200+
data = bytes.fromhex(f'{i:x}')
201+
for endian in (BIG_ENDIAN, LITTLE_ENDIAN):
202+
with self.subTest(data=data, size=size, endian=endian):
203+
data1 = data if endian == BIG_ENDIAN else data[::-1]
204+
value = unpack(data1, endian)
205+
if signaling and sys.platform == 'win32':
206+
# On this platform sNaN becomes qNaN when returned
207+
# from function. That's a known bug, e.g.
208+
# https://developercommunity.visualstudio.com/t/155064
209+
# (see also gh-130317).
210+
value = _testcapi.float_set_snan(value)
211+
data2 = pack(size, value, endian)
212+
self.assertTrue(math.isnan(value))
213+
self.assertEqual(data1, data2)
214+
181215

182216
if __name__ == "__main__":
183217
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :c:func:`PyFloat_Pack2` and :c:func:`PyFloat_Unpack2` for NaN's with
2+
payload. This corrects round-trip for :func:`struct.unpack` and
3+
:func:`struct.pack` in case of the IEEE 754 binary16 "half precision" type.
4+
Patch by Sergey B Kirpichev.

Modules/_testcapi/clinic/float.c.h

+10-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/_testcapi/float.c

+30
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,39 @@ test_string_to_double(PyObject *self, PyObject *Py_UNUSED(ignored))
157157
}
158158

159159

160+
/*[clinic input]
161+
_testcapi.float_set_snan
162+
163+
obj: object
164+
/
165+
166+
Make a signaling NaN.
167+
[clinic start generated code]*/
168+
169+
static PyObject *
170+
_testcapi_float_set_snan(PyObject *module, PyObject *obj)
171+
/*[clinic end generated code: output=f43778a70f60aa4b input=c1269b0f88ef27ac]*/
172+
{
173+
if (!PyFloat_Check(obj)) {
174+
PyErr_SetString(PyExc_ValueError, "float-point number expected");
175+
return NULL;
176+
}
177+
double d = ((PyFloatObject *)obj)->ob_fval;
178+
if (!isnan(d)) {
179+
PyErr_SetString(PyExc_ValueError, "nan expected");
180+
return NULL;
181+
}
182+
uint64_t v;
183+
memcpy(&v, &d, 8);
184+
v &= ~(1ULL << 51); /* make sNaN */
185+
memcpy(&d, &v, 8);
186+
return PyFloat_FromDouble(d);
187+
}
188+
160189
static PyMethodDef test_methods[] = {
161190
_TESTCAPI_FLOAT_PACK_METHODDEF
162191
_TESTCAPI_FLOAT_UNPACK_METHODDEF
192+
_TESTCAPI_FLOAT_SET_SNAN_METHODDEF
163193
{"test_string_to_double", test_string_to_double, METH_NOARGS},
164194
{NULL},
165195
};

Objects/floatobject.c

+42-7
Original file line numberDiff line numberDiff line change
@@ -2021,14 +2021,13 @@ PyFloat_Pack2(double x, char *data, int le)
20212021
bits = 0;
20222022
}
20232023
else if (isnan(x)) {
2024-
/* There are 2046 distinct half-precision NaNs (1022 signaling and
2025-
1024 quiet), but there are only two quiet NaNs that don't arise by
2026-
quieting a signaling NaN; we get those by setting the topmost bit
2027-
of the fraction field and clearing all other fraction bits. We
2028-
choose the one with the appropriate sign. */
20292024
sign = (copysign(1.0, x) == -1.0);
20302025
e = 0x1f;
2031-
bits = 512;
2026+
2027+
uint64_t v;
2028+
memcpy(&v, &x, sizeof(v));
2029+
v &= 0xffc0000000000ULL;
2030+
bits = (unsigned short)(v >> 42); /* NaN's type & payload */
20322031
}
20332032
else {
20342033
sign = (x < 0.0);
@@ -2192,6 +2191,21 @@ PyFloat_Pack4(double x, char *data, int le)
21922191
if (isinf(y) && !isinf(x))
21932192
goto Overflow;
21942193

2194+
/* correct y if x was a sNaN, transformed to qNaN by conversion */
2195+
if (isnan(x)) {
2196+
uint64_t v;
2197+
2198+
memcpy(&v, &x, 8);
2199+
if ((v & (1ULL << 51)) == 0) {
2200+
union float_val {
2201+
float f;
2202+
uint32_t u32;
2203+
} *py = (union float_val *)&y;
2204+
2205+
py->u32 &= ~(1 << 22); /* make sNaN */
2206+
}
2207+
}
2208+
21952209
unsigned char s[sizeof(float)];
21962210
memcpy(s, &y, sizeof(float));
21972211

@@ -2374,7 +2388,11 @@ PyFloat_Unpack2(const char *data, int le)
23742388
}
23752389
else {
23762390
/* NaN */
2377-
return sign ? -fabs(Py_NAN) : fabs(Py_NAN);
2391+
uint64_t v = sign ? 0xfff0000000000000ULL : 0x7ff0000000000000ULL;
2392+
2393+
v += (uint64_t)f << 42; /* add NaN's type & payload */
2394+
memcpy(&x, &v, sizeof(v));
2395+
return x;
23782396
}
23792397
}
23802398

@@ -2470,6 +2488,23 @@ PyFloat_Unpack4(const char *data, int le)
24702488
memcpy(&x, p, 4);
24712489
}
24722490

2491+
/* return sNaN double if x was sNaN float */
2492+
if (isnan(x)) {
2493+
uint32_t v;
2494+
memcpy(&v, &x, 4);
2495+
2496+
if ((v & (1 << 22)) == 0) {
2497+
double y = x; /* will make qNaN double */
2498+
union double_val {
2499+
double d;
2500+
uint64_t u64;
2501+
} *py = (union double_val *)&y;
2502+
2503+
py->u64 &= ~(1ULL << 51); /* make sNaN */
2504+
return y;
2505+
}
2506+
}
2507+
24732508
return x;
24742509
}
24752510
}

0 commit comments

Comments
 (0)