Skip to content

Commit e9c11b7

Browse files
[3.14] pythongh-141042: fix sNaN's packing for mixed floating-point formats (pythonGH-141107) (python#141459)
pythongh-141042: fix sNaN's packing for mixed floating-point formats (pythonGH-141107) (cherry picked from commit 23d85a2) Co-authored-by: Sergey B Kirpichev <[email protected]>
1 parent 75b5157 commit e9c11b7

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
@@ -2028,6 +2028,10 @@ PyFloat_Pack2(double x, char *data, int le)
20282028
memcpy(&v, &x, sizeof(v));
20292029
v &= 0xffc0000000000ULL;
20302030
bits = (unsigned short)(v >> 42); /* NaN's type & payload */
2031+
/* set qNaN if no payload */
2032+
if (!bits) {
2033+
bits |= (1<<9);
2034+
}
20312035
}
20322036
else {
20332037
sign = (x < 0.0);
@@ -2200,16 +2204,16 @@ PyFloat_Pack4(double x, char *data, int le)
22002204
if ((v & (1ULL << 51)) == 0) {
22012205
uint32_t u32;
22022206
memcpy(&u32, &y, 4);
2203-
u32 &= ~(1 << 22); /* make sNaN */
2207+
/* if have payload, make sNaN */
2208+
if (u32 & 0x3fffff) {
2209+
u32 &= ~(1 << 22);
2210+
}
22042211
memcpy(&y, &u32, 4);
22052212
}
22062213
#else
22072214
uint32_t u32;
22082215

22092216
memcpy(&u32, &y, 4);
2210-
if ((v & (1ULL << 51)) == 0) {
2211-
u32 &= ~(1 << 22);
2212-
}
22132217
/* Workaround RISC-V: "If a NaN value is converted to a
22142218
* different floating-point type, the result is the
22152219
* canonical NaN of the new type". The canonical NaN here
@@ -2220,6 +2224,10 @@ PyFloat_Pack4(double x, char *data, int le)
22202224
/* add payload */
22212225
u32 -= (u32 & 0x3fffff);
22222226
u32 += (uint32_t)((v & 0x7ffffffffffffULL) >> 29);
2227+
/* if have payload, make sNaN */
2228+
if ((v & (1ULL << 51)) == 0 && (u32 & 0x3fffff)) {
2229+
u32 &= ~(1 << 22);
2230+
}
22232231

22242232
memcpy(&y, &u32, 4);
22252233
#endif

0 commit comments

Comments
 (0)