Skip to content

Commit ad2f088

Browse files
authored
gh-130317: Fix test_pack_unpack_roundtrip() and add docs (#133204)
* Skip sNaN's testing in 32-bit mode. * Drop float_set_snan() helper. * Use memcpy() workaround for sNaN's in PyFloat_Unpack4(). * Document, that sNaN's may not be preserved by PyFloat_Pack/Unpack API.
1 parent ed039b8 commit ad2f088

File tree

5 files changed

+18
-62
lines changed

5 files changed

+18
-62
lines changed

Doc/c-api/float.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ NaNs (if such things exist on the platform) isn't handled correctly, and
9696
attempting to unpack a bytes string containing an IEEE INF or NaN will raise an
9797
exception.
9898
99+
Note that NaNs type may not be preserved on IEEE platforms (silent NaN become
100+
quiet), for example on x86 systems in 32-bit mode.
101+
99102
On non-IEEE platforms with more precision, or larger dynamic range, than IEEE
100103
754 supports, not all values can be packed; on non-IEEE platforms with less
101104
precision, or smaller dynamic range, not all values can be unpacked. What

Lib/test/test_capi/test_float.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -180,20 +180,23 @@ def test_pack_unpack_roundtrip(self):
180180
self.assertEqual(value2, value)
181181

182182
@unittest.skipUnless(HAVE_IEEE_754, "requires IEEE 754")
183-
# Skip on x86 (32-bit), since these tests fail. The problem is that sNaN
184-
# doubles become qNaN doubles just by the C calling convention, there is no
185-
# way to preserve sNaN doubles between C function calls. But tests pass
186-
# on Windows x86.
187-
@unittest.skipIf((sys.maxsize == 2147483647) and not(sys.platform == 'win32'),
188-
'test fails on x86 (32-bit)')
189183
def test_pack_unpack_roundtrip_for_nans(self):
190184
pack = _testcapi.float_pack
191185
unpack = _testcapi.float_unpack
192186

193187
for _ in range(10):
194188
for size in (2, 4, 8):
195189
sign = random.randint(0, 1)
196-
signaling = random.randint(0, 1)
190+
if sys.maxsize != 2147483647: # not it 32-bit mode
191+
signaling = random.randint(0, 1)
192+
else:
193+
# Skip sNaN's on x86 (32-bit). The problem is that sNaN
194+
# doubles become qNaN doubles just by the C calling
195+
# convention, there is no way to preserve sNaN doubles
196+
# between C function calls with the current
197+
# PyFloat_Pack/Unpack*() API. See also gh-130317 and
198+
# e.g. https://developercommunity.visualstudio.com/t/155064
199+
signaling = 0
197200
quiet = int(not signaling)
198201
if size == 8:
199202
payload = random.randint(signaling, 1 << 50)
@@ -209,12 +212,6 @@ def test_pack_unpack_roundtrip_for_nans(self):
209212
with self.subTest(data=data, size=size, endian=endian):
210213
data1 = data if endian == BIG_ENDIAN else data[::-1]
211214
value = unpack(data1, endian)
212-
if signaling and sys.platform == 'win32':
213-
# On Windows x86, sNaN becomes qNaN when returned
214-
# from function. That's a known bug, e.g.
215-
# https://developercommunity.visualstudio.com/t/155064
216-
# (see also gh-130317).
217-
value = _testcapi.float_set_snan(value)
218215
data2 = pack(size, value, endian)
219216
self.assertTrue(math.isnan(value))
220217
self.assertEqual(data1, data2)

Modules/_testcapi/clinic/float.c.h

Lines changed: 1 addition & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/_testcapi/float.c

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -157,42 +157,9 @@ 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-
186-
// gh-130317: memcpy() is needed to preserve the sNaN flag on x86 (32-bit)
187-
PyObject *res = PyFloat_FromDouble(0.0);
188-
memcpy(&((PyFloatObject *)res)->ob_fval, &v, 8);
189-
return res;
190-
}
191-
192160
static PyMethodDef test_methods[] = {
193161
_TESTCAPI_FLOAT_PACK_METHODDEF
194162
_TESTCAPI_FLOAT_UNPACK_METHODDEF
195-
_TESTCAPI_FLOAT_SET_SNAN_METHODDEF
196163
{"test_string_to_double", test_string_to_double, METH_NOARGS},
197164
{NULL},
198165
};

Objects/floatobject.c

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2495,12 +2495,10 @@ PyFloat_Unpack4(const char *data, int le)
24952495

24962496
if ((v & (1 << 22)) == 0) {
24972497
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 */
2498+
uint64_t u64;
2499+
memcpy(&u64, &y, 8);
2500+
u64 &= ~(1ULL << 51); /* make sNaN */
2501+
memcpy(&y, &u64, 8);
25042502
return y;
25052503
}
25062504
}

0 commit comments

Comments
 (0)