https://github.com/python/cpython/commit/e9c11b749576dfb0eb2b1a40ae84f532f1526df5
commit: e9c11b749576dfb0eb2b1a40ae84f532f1526df5
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: vstinner <[email protected]>
date: 2025-11-12T10:33:34Z
summary:

[3.14] gh-141042: fix sNaN's packing for mixed floating-point formats 
(GH-141107) (#141459)

gh-141042: fix sNaN's packing for mixed floating-point formats (GH-141107)
(cherry picked from commit 23d85a2a3fb029172ea15c6e596f64f8c2868ed3)

Co-authored-by: Sergey B Kirpichev <[email protected]>

files:
A Misc/NEWS.d/next/C_API/2025-11-06-06-28-14.gh-issue-141042.brOioJ.rst
M Lib/test/test_capi/test_float.py
M Objects/floatobject.c

diff --git a/Lib/test/test_capi/test_float.py b/Lib/test/test_capi/test_float.py
index 983b991b4f163d..df7017e6436a69 100644
--- a/Lib/test/test_capi/test_float.py
+++ b/Lib/test/test_capi/test_float.py
@@ -29,6 +29,23 @@
 NAN = float("nan")
 
 
+def make_nan(size, sign, quiet, payload=None):
+    if size == 8:
+        payload_mask = 0x7ffffffffffff
+        i = (sign << 63) + (0x7ff << 52) + (quiet << 51)
+    elif size == 4:
+        payload_mask = 0x3fffff
+        i = (sign << 31) + (0xff << 23) + (quiet << 22)
+    elif size == 2:
+        payload_mask = 0x1ff
+        i = (sign << 15) + (0x1f << 10) + (quiet << 9)
+    else:
+        raise ValueError("size must be either 2, 4, or 8")
+    if payload is None:
+        payload = random.randint(not quiet, payload_mask)
+    return i + payload
+
+
 class CAPIFloatTest(unittest.TestCase):
     def test_check(self):
         # Test PyFloat_Check()
@@ -202,16 +219,7 @@ def test_pack_unpack_roundtrip_for_nans(self):
                         # HP PA RISC uses 0 for quiet, see:
                         # https://en.wikipedia.org/wiki/NaN#Encoding
                         signaling = 1
-                quiet = int(not signaling)
-                if size == 8:
-                    payload = random.randint(signaling, 0x7ffffffffffff)
-                    i = (sign << 63) + (0x7ff << 52) + (quiet << 51) + payload
-                elif size == 4:
-                    payload = random.randint(signaling, 0x3fffff)
-                    i = (sign << 31) + (0xff << 23) + (quiet << 22) + payload
-                elif size == 2:
-                    payload = random.randint(signaling, 0x1ff)
-                    i = (sign << 15) + (0x1f << 10) + (quiet << 9) + payload
+                i = make_nan(size, sign, not signaling)
                 data = bytes.fromhex(f'{i:x}')
                 for endian in (BIG_ENDIAN, LITTLE_ENDIAN):
                     with self.subTest(data=data, size=size, endian=endian):
@@ -221,6 +229,32 @@ def test_pack_unpack_roundtrip_for_nans(self):
                         self.assertTrue(math.isnan(value))
                         self.assertEqual(data1, data2)
 
+    @unittest.skipUnless(HAVE_IEEE_754, "requires IEEE 754")
+    @unittest.skipUnless(sys.maxsize != 2147483647, "requires 64-bit mode")
+    def test_pack_unpack_nans_for_different_formats(self):
+        pack = _testcapi.float_pack
+        unpack = _testcapi.float_unpack
+
+        for endian in (BIG_ENDIAN, LITTLE_ENDIAN):
+            with self.subTest(endian=endian):
+                byteorder = "big" if endian == BIG_ENDIAN else "little"
+
+                # Convert sNaN to qNaN, if payload got truncated
+                data = make_nan(8, 0, False, 0x80001).to_bytes(8, byteorder)
+                snan_low = unpack(data, endian)
+                qnan4 = make_nan(4, 0, True, 0).to_bytes(4, byteorder)
+                qnan2 = make_nan(2, 0, True, 0).to_bytes(2, byteorder)
+                self.assertEqual(pack(4, snan_low, endian), qnan4)
+                self.assertEqual(pack(2, snan_low, endian), qnan2)
+
+                # Preserve NaN type, if payload not truncated
+                data = make_nan(8, 0, False, 0x80000000001).to_bytes(8, 
byteorder)
+                snan_high = unpack(data, endian)
+                snan4 = make_nan(4, 0, False, 16384).to_bytes(4, byteorder)
+                snan2 = make_nan(2, 0, False, 2).to_bytes(2, byteorder)
+                self.assertEqual(pack(4, snan_high, endian), snan4)
+                self.assertEqual(pack(2, snan_high, endian), snan2)
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git 
a/Misc/NEWS.d/next/C_API/2025-11-06-06-28-14.gh-issue-141042.brOioJ.rst 
b/Misc/NEWS.d/next/C_API/2025-11-06-06-28-14.gh-issue-141042.brOioJ.rst
new file mode 100644
index 00000000000000..22a1aa1f405318
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2025-11-06-06-28-14.gh-issue-141042.brOioJ.rst
@@ -0,0 +1,3 @@
+Make qNaN in :c:func:`PyFloat_Pack2` and :c:func:`PyFloat_Pack4`, if while
+conversion to a narrower precision floating-point format --- the remaining
+after truncation payload will be zero.  Patch by Sergey B Kirpichev.
diff --git a/Objects/floatobject.c b/Objects/floatobject.c
index 93e1973d6b32fc..700b8d4cbeb9fd 100644
--- a/Objects/floatobject.c
+++ b/Objects/floatobject.c
@@ -2028,6 +2028,10 @@ PyFloat_Pack2(double x, char *data, int le)
         memcpy(&v, &x, sizeof(v));
         v &= 0xffc0000000000ULL;
         bits = (unsigned short)(v >> 42); /* NaN's type & payload */
+        /* set qNaN if no payload */
+        if (!bits) {
+            bits |= (1<<9);
+        }
     }
     else {
         sign = (x < 0.0);
@@ -2200,16 +2204,16 @@ PyFloat_Pack4(double x, char *data, int le)
             if ((v & (1ULL << 51)) == 0) {
                 uint32_t u32;
                 memcpy(&u32, &y, 4);
-                u32 &= ~(1 << 22); /* make sNaN */
+                /* if have payload, make sNaN */
+                if (u32 & 0x3fffff) {
+                    u32 &= ~(1 << 22);
+                }
                 memcpy(&y, &u32, 4);
             }
 #else
             uint32_t u32;
 
             memcpy(&u32, &y, 4);
-            if ((v & (1ULL << 51)) == 0) {
-                u32 &= ~(1 << 22);
-            }
             /* Workaround RISC-V: "If a NaN value is converted to a
              * different floating-point type, the result is the
              * canonical NaN of the new type".  The canonical NaN here
@@ -2220,6 +2224,10 @@ PyFloat_Pack4(double x, char *data, int le)
             /* add payload */
             u32 -= (u32 & 0x3fffff);
             u32 += (uint32_t)((v & 0x7ffffffffffffULL) >> 29);
+            /* if have payload, make sNaN */
+            if ((v & (1ULL << 51)) == 0 && (u32 & 0x3fffff)) {
+                u32 &= ~(1 << 22);
+            }
 
             memcpy(&y, &u32, 4);
 #endif

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to