Skip to content

Commit 23d85a2

Browse files
authored
pythongh-141042: fix sNaN's packing for mixed floating-point formats (python#141107)
1 parent 7d54374 commit 23d85a2

File tree

3 files changed

+59
-14
lines changed

3 files changed

+59
-14
lines changed

Lib/test/test_capi/test_float.py

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,23 @@
2929
NAN = float("nan")
3030

3131

32+
def make_nan(size, sign, quiet, payload=None):
33+
if size == 8:
34+
payload_mask = 0x7ffffffffffff
35+
i = (sign << 63) + (0x7ff << 52) + (quiet << 51)
36+
elif size == 4:
37+
payload_mask = 0x3fffff
38+
i = (sign << 31) + (0xff << 23) + (quiet << 22)
39+
elif size == 2:
40+
payload_mask = 0x1ff
41+
i = (sign << 15) + (0x1f << 10) + (quiet << 9)
42+
else:
43+
raise ValueError("size must be either 2, 4, or 8")
44+
if payload is None:
45+
payload = random.randint(not quiet, payload_mask)
46+
return i + payload
47+
48+
3249
class CAPIFloatTest(unittest.TestCase):
3350
def test_check(self):
3451
# Test PyFloat_Check()
@@ -202,16 +219,7 @@ def test_pack_unpack_roundtrip_for_nans(self):
202219
# HP PA RISC uses 0 for quiet, see:
203220
# https://en.wikipedia.org/wiki/NaN#Encoding
204221
signaling = 1
205-
quiet = int(not signaling)
206-
if size == 8:
207-
payload = random.randint(signaling, 0x7ffffffffffff)
208-
i = (sign << 63) + (0x7ff << 52) + (quiet << 51) + payload
209-
elif size == 4:
210-
payload = random.randint(signaling, 0x3fffff)
211-
i = (sign << 31) + (0xff << 23) + (quiet << 22) + payload
212-
elif size == 2:
213-
payload = random.randint(signaling, 0x1ff)
214-
i = (sign << 15) + (0x1f << 10) + (quiet << 9) + payload
222+
i = make_nan(size, sign, not signaling)
215223
data = bytes.fromhex(f'{i:x}')
216224
for endian in (BIG_ENDIAN, LITTLE_ENDIAN):
217225
with self.subTest(data=data, size=size, endian=endian):
@@ -221,6 +229,32 @@ def test_pack_unpack_roundtrip_for_nans(self):
221229
self.assertTrue(math.isnan(value))
222230
self.assertEqual(data1, data2)
223231

232+
@unittest.skipUnless(HAVE_IEEE_754, "requires IEEE 754")
233+
@unittest.skipUnless(sys.maxsize != 2147483647, "requires 64-bit mode")
234+
def test_pack_unpack_nans_for_different_formats(self):
235+
pack = _testcapi.float_pack
236+
unpack = _testcapi.float_unpack
237+
238+
for endian in (BIG_ENDIAN, LITTLE_ENDIAN):
239+
with self.subTest(endian=endian):
240+
byteorder = "big" if endian == BIG_ENDIAN else "little"
241+
242+
# Convert sNaN to qNaN, if payload got truncated
243+
data = make_nan(8, 0, False, 0x80001).to_bytes(8, byteorder)
244+
snan_low = unpack(data, endian)
245+
qnan4 = make_nan(4, 0, True, 0).to_bytes(4, byteorder)
246+
qnan2 = make_nan(2, 0, True, 0).to_bytes(2, byteorder)
247+
self.assertEqual(pack(4, snan_low, endian), qnan4)
248+
self.assertEqual(pack(2, snan_low, endian), qnan2)
249+
250+
# Preserve NaN type, if payload not truncated
251+
data = make_nan(8, 0, False, 0x80000000001).to_bytes(8, byteorder)
252+
snan_high = unpack(data, endian)
253+
snan4 = make_nan(4, 0, False, 16384).to_bytes(4, byteorder)
254+
snan2 = make_nan(2, 0, False, 2).to_bytes(2, byteorder)
255+
self.assertEqual(pack(4, snan_high, endian), snan4)
256+
self.assertEqual(pack(2, snan_high, endian), snan2)
257+
224258

225259
if __name__ == "__main__":
226260
unittest.main()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Make qNaN in :c:func:`PyFloat_Pack2` and :c:func:`PyFloat_Pack4`, if while
2+
conversion to a narrower precision floating-point format --- the remaining
3+
after truncation payload will be zero. Patch by Sergey B Kirpichev.

Objects/floatobject.c

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2030,6 +2030,10 @@ PyFloat_Pack2(double x, char *data, int le)
20302030
memcpy(&v, &x, sizeof(v));
20312031
v &= 0xffc0000000000ULL;
20322032
bits = (unsigned short)(v >> 42); /* NaN's type & payload */
2033+
/* set qNaN if no payload */
2034+
if (!bits) {
2035+
bits |= (1<<9);
2036+
}
20332037
}
20342038
else {
20352039
sign = (x < 0.0);
@@ -2202,16 +2206,16 @@ PyFloat_Pack4(double x, char *data, int le)
22022206
if ((v & (1ULL << 51)) == 0) {
22032207
uint32_t u32;
22042208
memcpy(&u32, &y, 4);
2205-
u32 &= ~(1 << 22); /* make sNaN */
2209+
/* if have payload, make sNaN */
2210+
if (u32 & 0x3fffff) {
2211+
u32 &= ~(1 << 22);
2212+
}
22062213
memcpy(&y, &u32, 4);
22072214
}
22082215
#else
22092216
uint32_t u32;
22102217

22112218
memcpy(&u32, &y, 4);
2212-
if ((v & (1ULL << 51)) == 0) {
2213-
u32 &= ~(1 << 22);
2214-
}
22152219
/* Workaround RISC-V: "If a NaN value is converted to a
22162220
* different floating-point type, the result is the
22172221
* canonical NaN of the new type". The canonical NaN here
@@ -2222,6 +2226,10 @@ PyFloat_Pack4(double x, char *data, int le)
22222226
/* add payload */
22232227
u32 -= (u32 & 0x3fffff);
22242228
u32 += (uint32_t)((v & 0x7ffffffffffffULL) >> 29);
2229+
/* if have payload, make sNaN */
2230+
if ((v & (1ULL << 51)) == 0 && (u32 & 0x3fffff)) {
2231+
u32 &= ~(1 << 22);
2232+
}
22252233

22262234
memcpy(&y, &u32, 4);
22272235
#endif

0 commit comments

Comments
 (0)