Skip to content

BUG: Fix casting from longdouble to long #9971

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 8, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions numpy/core/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,7 @@ def get_mathlib_info(*args):
join('src', 'private', 'templ_common.h.src'),
join('src', 'private', 'lowlevel_strided_loops.h'),
join('src', 'private', 'mem_overlap.h'),
join('src', 'private', 'npy_longdouble.h'),
join('src', 'private', 'ufunc_override.h'),
join('src', 'private', 'binop_override.h'),
join('src', 'private', 'npy_extint128.h'),
Expand Down Expand Up @@ -827,6 +828,7 @@ def get_mathlib_info(*args):
join('src', 'multiarray', 'vdot.c'),
join('src', 'private', 'templ_common.h.src'),
join('src', 'private', 'mem_overlap.c'),
join('src', 'private', 'npy_longdouble.c'),
join('src', 'private', 'ufunc_override.c'),
]

Expand Down Expand Up @@ -884,6 +886,7 @@ def generate_umath_c(ext, build_dir):
join('src', 'umath', 'ufunc_type_resolution.c'),
join('src', 'umath', 'override.c'),
join('src', 'private', 'mem_overlap.c'),
join('src', 'private', 'npy_longdouble.c'),
join('src', 'private', 'ufunc_override.c')]

umath_deps = [
Expand All @@ -897,6 +900,7 @@ def generate_umath_c(ext, build_dir):
join(codegen_dir, 'generate_ufunc_api.py'),
join('src', 'private', 'lowlevel_strided_loops.h'),
join('src', 'private', 'mem_overlap.h'),
join('src', 'private', 'npy_longdouble.h'),
join('src', 'private', 'ufunc_override.h'),
join('src', 'private', 'binop_override.h')] + npymath_sources

Expand Down
10 changes: 2 additions & 8 deletions numpy/core/src/multiarray/scalartypes.c.src
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include "alloc.h"
#include "npy_import.h"
#include "dragon4.h"
#include "npy_longdouble.h"

#include <stdlib.h>

Expand Down Expand Up @@ -832,14 +833,7 @@ static PyObject *
@char@longdoubletype_long(PyObject *self)
{
npy_longdouble val = PyArrayScalar_VAL(self, @CHAR@LongDouble)@POST@;

/*
* This raises OverflowError when the cast overflows to infinity (gh-9964)
*
* Could fix this with a PyLong_FromLongDouble(longdouble ldval)
* but this would need some more work...
*/
return PyLong_FromDouble((double) val);
return npy_longdouble_to_PyLong(val);
}

#if !defined(NPY_PY3K)
Expand Down
99 changes: 99 additions & 0 deletions numpy/core/src/private/npy_longdouble.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#include <Python.h>

#define NPY_NO_DEPRECATED_API NPY_API_VERSION
#include "numpy/ndarraytypes.h"
#include "numpy/npy_math.h"

/* This is a backport of Py_SETREF */
#define NPY_SETREF(op, op2) \
do { \
PyObject *_py_tmp = (PyObject *)(op); \
(op) = (op2); \
Py_DECREF(_py_tmp); \
} while (0)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would probably be generally handy throughout the codebase.

A job for another PR: would there be a better place to put it, and do we want to cal lit Py_SETREF and #ifndef Py_SETREF it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we decide to use it I agree that doing and #ifndef seems reasonable.

For anyone else reading, it looks like this python macro was created in python3.4, and there are a lot of discussions of it on the python mailing list and on the python issue tracker. It is still going through some changes as of april 2016, I think they decided to split it into two macros Py_SETREF and Py_XSETREF.

One motivation for it is to avoid a certain kind of bug, according to the comment in CPython. On the other hand it's not part of the "stable api", so there are some risks in using it.



/* Heavily derived from PyLong_FromDouble
* Notably, we can't set the digits directly, so have to shift and or instead.
*/
PyObject *
npy_longdouble_to_PyLong(npy_longdouble ldval)
{
PyObject *v;
PyObject *l_chunk_size;
// number of bits to extract at a time. CPython uses 30, but that's because
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, should be C style comment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Careless, good spot. This is the reason for the Mac failure?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. At the moment I'm suspecting the power function.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is this line: huge_ld = 1234 * np.longdouble(2) ** exp. We need someone running on a Mac to check this out. Maybe a fallback double function is being used somewhere.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it is, powl is blacklisted on the Mac

/* powl gives zero division warning on OS X, see gh-8307 */
#if defined(HAVE_POWL) && defined(NPY_OS_DARWIN)
#undef HAVE_POWL
#endif

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I don't see why that should be a problem, the double value of the fallback function should be OK with result cast to long double, so the multiplication should work. Maybe inlining is failing somewhere ...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fired up the old Mac. The Mac double pow function errors with exp == 1024, Linux works with that. I'm simply going to use exp = 1023 as a fix in this case.

Copy link
Member Author

@eric-wieser eric-wieser Nov 10, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exp=1023 doesn't work, because then the result fits in double - the goal here is to produce a longdouble that exceeds the limits for a double.

I suppose using exp=1023 and multiplying by 2 would work

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is what I did. See #10000 . Oh, looky there, issue 10000, it's like watching the odometer turn over ...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guess we will eventually find out if this all works for IBM double double.

// it's tied to the internal long representation
const int chunk_size = NPY_BITSOF_LONGLONG;
npy_longdouble frac;
int i, ndig, expo, neg;
neg = 0;

if (npy_isinf(ldval)) {
PyErr_SetString(PyExc_OverflowError,
"cannot convert longdouble infinity to integer");
return NULL;
}
if (npy_isnan(ldval)) {
PyErr_SetString(PyExc_ValueError,
"cannot convert longdouble NaN to integer");
return NULL;
}
if (ldval < 0.0) {
neg = 1;
ldval = -ldval;
}
frac = npy_frexpl(ldval, &expo); /* ldval = frac*2**expo; 0.0 <= frac < 1.0 */
v = PyLong_FromLong(0L);
if (v == NULL)
return NULL;
if (expo <= 0)
return v;

ndig = (expo-1) / chunk_size + 1;

l_chunk_size = PyLong_FromLong(chunk_size);
if (l_chunk_size == NULL) {
Py_DECREF(v);
return NULL;
}

/* Get the MSBs of the integral part of the float */
frac = npy_ldexpl(frac, (expo-1) % chunk_size + 1);
for (i = ndig; --i >= 0; ) {
npy_ulonglong chunk = (npy_ulonglong)frac;
PyObject *l_chunk;
/* v = v << chunk_size */
NPY_SETREF(v, PyNumber_Lshift(v, l_chunk_size));
if (v == NULL) {
goto done;
}
l_chunk = PyLong_FromUnsignedLongLong(chunk);
if (l_chunk == NULL) {
Py_DECREF(v);
v = NULL;
goto done;
}
/* v = v | chunk */
NPY_SETREF(v, PyNumber_Or(v, l_chunk));
Py_DECREF(l_chunk);
if (v == NULL) {
goto done;
}

/* Remove the msbs, and repeat */
frac = frac - (npy_longdouble) chunk;
frac = npy_ldexpl(frac, chunk_size);
}

/* v = -v */
if (neg) {
NPY_SETREF(v, PyNumber_Negative(v));
if (v == NULL) {
goto done;
}
}

done:
Py_DECREF(l_chunk_size);
return v;
}
Copy link
Member

@ahaldane ahaldane Nov 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a funny character here. Windows newline? OK, it says "No newline at end of file", so no problem.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Newline added

17 changes: 17 additions & 0 deletions numpy/core/src/private/npy_longdouble.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#ifndef __NPY_LONGDOUBLE_H
#define __NPY_LONGDOUBLE_H

#include "npy_config.h"
#include "numpy/ndarraytypes.h"

/* Convert a npy_longdouble to a python `long` integer.
*
* Results are rounded towards zero.
*
* This performs the same task as PyLong_FromDouble, but for long doubles
* which have a greater range.
*/
NPY_NO_EXPORT PyObject *
npy_longdouble_to_PyLong(npy_longdouble ldval);

#endif
52 changes: 25 additions & 27 deletions numpy/core/src/umath/scalarmath.c.src
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "templ_common.h"

#include "binop_override.h"
#include "npy_longdouble.h"

/* Basic operations:
*
Expand Down Expand Up @@ -1391,45 +1392,42 @@ emit_complexwarning(void)
*
* #cmplx = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1#
* #sign = (signed, unsigned)*5, , , , , , , #
* #unsigntyp = 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0*7#
* #ctype = long*8, PY_LONG_LONG*2, double*7#
* #ctype = long*8, PY_LONG_LONG*2,
* double*3, npy_longdouble, double*2, npy_longdouble#
* #to_ctype = , , , , , , , , , , npy_half_to_double, , , , , , #
* #realtyp = 0*10, 1*7#
* #func = (PyLong_FromLong, PyLong_FromUnsignedLong)*4,
* PyLong_FromLongLong, PyLong_FromUnsignedLongLong,
* PyLong_FromDouble*7#
* PyLong_FromDouble*3, npy_longdouble_to_PyLong,
* PyLong_FromDouble*2, npy_longdouble_to_PyLong#
*/
static PyObject *
@name@_int(PyObject *obj)
{
PyObject *long_result;

#if @cmplx@
@sign@ @ctype@ x= @to_ctype@(PyArrayScalar_VAL(obj, @Name@).real);
int ret;
@sign@ @ctype@ x = @to_ctype@(PyArrayScalar_VAL(obj, @Name@).real);
#else
@sign@ @ctype@ x= @to_ctype@(PyArrayScalar_VAL(obj, @Name@));
#endif

#if @realtyp@
double ix;
modf(x, &ix);
x = ix;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seemed to be just for the long/int choice below, which is far easier (although a little slower) to handle by just producing a long, and letting python decide whether to downcast to an int.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can now remove the @realtype@ repeat var. @unsigntyp@ too

(I don't exactly understand why these lines were needed before, since C-casting truncates the double fractional part anyway.. but the new code looks correct so I won't worry too much).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I think it was probably so the x < LONG_MAX and x < LONG_MIN checks aren't affected by the fractional value, in the case of exact equality of the integer part of x to those values.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, that was the purpose - but I don't think the decrease in clarity is worth it for a marginal performance improvement on only python 2.

I'll remove the repeat vars - good catch

@sign@ @ctype@ x = @to_ctype@(PyArrayScalar_VAL(obj, @Name@));
#endif

#if @cmplx@
ret = emit_complexwarning();
if (ret < 0) {
if (emit_complexwarning() < 0) {
return NULL;
}
#endif

#if @unsigntyp@
if(x < LONG_MAX)
return PyInt_FromLong(x);
#else
if(LONG_MIN < x && x < LONG_MAX)
return PyInt_FromLong(x);
long_result = @func@(x);
if (long_result == NULL){
return NULL;
}

#ifndef NPY_PY3K
/* Invoke long.__int__ to try to downcast */
long_result = Py_TYPE(long_result)->tp_as_number->nb_int(long_result);
#endif
return @func@(x);

return long_result;
}
/**end repeat**/

Expand All @@ -1447,18 +1445,18 @@ static PyObject *
* #to_ctype = (, , , , , , , , , , npy_half_to_double, , , , , , )*2#
* #which = long*17, float*17#
* #func = (PyLong_FromLongLong, PyLong_FromUnsignedLongLong)*5,
* PyLong_FromDouble*7, PyFloat_FromDouble*17#
* PyLong_FromDouble*3, npy_longdouble_to_PyLong,
* PyLong_FromDouble*2, npy_longdouble_to_PyLong,
* PyFloat_FromDouble*17#
*/
static NPY_INLINE PyObject *
@name@_@which@(PyObject *obj)
{
#if @cmplx@
int ret;
ret = emit_complexwarning();
if (ret < 0) {
if (emit_complexwarning() < 0) {
return NULL;
}
return @func@(@to_ctype@((PyArrayScalar_VAL(obj, @Name@)).real));
return @func@(@to_ctype@(PyArrayScalar_VAL(obj, @Name@).real));
#else
return @func@(@to_ctype@(PyArrayScalar_VAL(obj, @Name@)));
#endif
Expand Down
26 changes: 21 additions & 5 deletions numpy/core/tests/test_scalarmath.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@

import numpy as np
from numpy.testing import (
run_module_suite, assert_, assert_equal, assert_raises,
assert_almost_equal, assert_allclose, assert_array_equal, IS_PYPY,
suppress_warnings, dec, _gen_alignment_data,
run_module_suite,
assert_, assert_equal, assert_raises,
assert_almost_equal, assert_allclose, assert_array_equal,
IS_PYPY, suppress_warnings, dec, _gen_alignment_data,
)

types = [np.bool_, np.byte, np.ubyte, np.short, np.ushort, np.intc, np.uintc,
Expand Down Expand Up @@ -398,7 +399,7 @@ def overflow_error_func(dtype):
for code in 'lLqQ':
assert_raises(OverflowError, overflow_error_func, code)

def test_longdouble_int(self):
def test_int_from_infinite_longdouble(self):
# gh-627
x = np.longdouble(np.inf)
assert_raises(OverflowError, int, x)
Expand All @@ -410,7 +411,7 @@ def test_longdouble_int(self):

@dec.knownfailureif(not IS_PYPY,
"__int__ is not the same as int in cpython (gh-9972)")
def test_clongdouble___int__(self):
def test_int_from_infinite_longdouble___int__(self):
x = np.longdouble(np.inf)
assert_raises(OverflowError, x.__int__)
with suppress_warnings() as sup:
Expand All @@ -419,6 +420,21 @@ def test_clongdouble___int__(self):
assert_raises(OverflowError, x.__int__)
assert_equal(len(sup.log), 1)

@dec.skipif(np.finfo(np.double) == np.finfo(np.longdouble))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test fails on ppc64.

I think I'm going to add @dec.skipif(platform.machine().startswith("ppc64")) here.

def test_int_from_huge_longdouble(self):
# produce a longdouble that would overflow a double
exp = np.finfo(np.double).maxexp
huge_ld = 1234 * np.longdouble(2) ** exp
huge_i = 1234 * 2 ** exp
assert_(huge_ld != np.inf)
assert_equal(int(huge_ld), huge_i)

def test_int_from_longdouble(self):
x = np.longdouble(1.5)
assert_equal(int(x), 1)
x = np.longdouble(-10.5)
assert_equal(int(x), -10)

def test_numpy_scalar_relational_operators(self):
# All integer
for dt1 in np.typecodes['AllInteger']:
Expand Down