gh-120950: Fix overflow in math.log() with large int-like argument (GH-121011)

Handling of arbitrary large int-like argument is now consistent with
handling arbitrary large int arguments.
This commit is contained in:
Serhiy Storchaka 2025-11-12 00:27:13 +02:00 committed by GitHub
parent 9e7340cd3b
commit 4359706ac8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 111 additions and 30 deletions

View file

@ -189,6 +189,22 @@ def __init__(self, value):
def __index__(self): def __index__(self):
return self.value return self.value
class IndexableFloatLike:
def __init__(self, float_value, index_value):
self.float_value = float_value
self.index_value = index_value
def __float__(self):
if isinstance(self.float_value, BaseException):
raise self.float_value
return self.float_value
def __index__(self):
if isinstance(self.index_value, BaseException):
raise self.index_value
return self.index_value
class BadDescr: class BadDescr:
def __get__(self, obj, objtype=None): def __get__(self, obj, objtype=None):
raise ValueError raise ValueError
@ -1192,13 +1208,32 @@ def testLog(self):
self.ftest('log(10**40, 10**20)', math.log(10**40, 10**20), 2) self.ftest('log(10**40, 10**20)', math.log(10**40, 10**20), 2)
self.ftest('log(10**1000)', math.log(10**1000), self.ftest('log(10**1000)', math.log(10**1000),
2302.5850929940457) 2302.5850929940457)
self.ftest('log(10**2000, 10**1000)', math.log(10**2000, 10**1000), 2)
self.ftest('log(MyIndexable(32), MyIndexable(2))',
math.log(MyIndexable(32), MyIndexable(2)), 5)
self.ftest('log(MyIndexable(10**1000))',
math.log(MyIndexable(10**1000)),
2302.5850929940457)
self.ftest('log(MyIndexable(10**2000), MyIndexable(10**1000))',
math.log(MyIndexable(10**2000), MyIndexable(10**1000)),
2)
self.assertRaises(ValueError, math.log, 0.0)
self.assertRaises(ValueError, math.log, 0)
self.assertRaises(ValueError, math.log, MyIndexable(0))
self.assertRaises(ValueError, math.log, -1.5) self.assertRaises(ValueError, math.log, -1.5)
self.assertRaises(ValueError, math.log, -1)
self.assertRaises(ValueError, math.log, MyIndexable(-1))
self.assertRaises(ValueError, math.log, -10**1000) self.assertRaises(ValueError, math.log, -10**1000)
self.assertRaises(ValueError, math.log, MyIndexable(-10**1000))
self.assertRaises(ValueError, math.log, 10, -10) self.assertRaises(ValueError, math.log, 10, -10)
self.assertRaises(ValueError, math.log, NINF) self.assertRaises(ValueError, math.log, NINF)
self.assertEqual(math.log(INF), INF) self.assertEqual(math.log(INF), INF)
self.assertTrue(math.isnan(math.log(NAN))) self.assertTrue(math.isnan(math.log(NAN)))
self.assertEqual(math.log(IndexableFloatLike(math.e, 10**1000)), 1.0)
self.assertAlmostEqual(math.log(IndexableFloatLike(OverflowError(), 10**1000)),
2302.5850929940457)
def testLog1p(self): def testLog1p(self):
self.assertRaises(TypeError, math.log1p) self.assertRaises(TypeError, math.log1p)
for n in [2, 2**90, 2**300]: for n in [2, 2**90, 2**300]:
@ -1214,16 +1249,28 @@ def testLog2(self):
self.assertEqual(math.log2(1), 0.0) self.assertEqual(math.log2(1), 0.0)
self.assertEqual(math.log2(2), 1.0) self.assertEqual(math.log2(2), 1.0)
self.assertEqual(math.log2(4), 2.0) self.assertEqual(math.log2(4), 2.0)
self.assertEqual(math.log2(MyIndexable(4)), 2.0)
# Large integer values # Large integer values
self.assertEqual(math.log2(2**1023), 1023.0) self.assertEqual(math.log2(2**1023), 1023.0)
self.assertEqual(math.log2(2**1024), 1024.0) self.assertEqual(math.log2(2**1024), 1024.0)
self.assertEqual(math.log2(2**2000), 2000.0) self.assertEqual(math.log2(2**2000), 2000.0)
self.assertEqual(math.log2(MyIndexable(2**2000)), 2000.0)
self.assertRaises(ValueError, math.log2, 0.0)
self.assertRaises(ValueError, math.log2, 0)
self.assertRaises(ValueError, math.log2, MyIndexable(0))
self.assertRaises(ValueError, math.log2, -1.5) self.assertRaises(ValueError, math.log2, -1.5)
self.assertRaises(ValueError, math.log2, -1)
self.assertRaises(ValueError, math.log2, MyIndexable(-1))
self.assertRaises(ValueError, math.log2, -2**2000)
self.assertRaises(ValueError, math.log2, MyIndexable(-2**2000))
self.assertRaises(ValueError, math.log2, NINF) self.assertRaises(ValueError, math.log2, NINF)
self.assertTrue(math.isnan(math.log2(NAN))) self.assertTrue(math.isnan(math.log2(NAN)))
self.assertEqual(math.log2(IndexableFloatLike(8.0, 2**2000)), 3.0)
self.assertEqual(math.log2(IndexableFloatLike(OverflowError(), 2**2000)), 2000.0)
@requires_IEEE_754 @requires_IEEE_754
# log2() is not accurate enough on Mac OS X Tiger (10.4) # log2() is not accurate enough on Mac OS X Tiger (10.4)
@support.requires_mac_ver(10, 5) @support.requires_mac_ver(10, 5)
@ -1239,12 +1286,24 @@ def testLog10(self):
self.ftest('log10(1)', math.log10(1), 0) self.ftest('log10(1)', math.log10(1), 0)
self.ftest('log10(10)', math.log10(10), 1) self.ftest('log10(10)', math.log10(10), 1)
self.ftest('log10(10**1000)', math.log10(10**1000), 1000.0) self.ftest('log10(10**1000)', math.log10(10**1000), 1000.0)
self.ftest('log10(MyIndexable(10))', math.log10(MyIndexable(10)), 1)
self.ftest('log10(MyIndexable(10**1000))',
math.log10(MyIndexable(10**1000)), 1000.0)
self.assertRaises(ValueError, math.log10, 0.0)
self.assertRaises(ValueError, math.log10, 0)
self.assertRaises(ValueError, math.log10, MyIndexable(0))
self.assertRaises(ValueError, math.log10, -1.5) self.assertRaises(ValueError, math.log10, -1.5)
self.assertRaises(ValueError, math.log10, -1)
self.assertRaises(ValueError, math.log10, MyIndexable(-1))
self.assertRaises(ValueError, math.log10, -10**1000) self.assertRaises(ValueError, math.log10, -10**1000)
self.assertRaises(ValueError, math.log10, MyIndexable(-10**1000))
self.assertRaises(ValueError, math.log10, NINF) self.assertRaises(ValueError, math.log10, NINF)
self.assertEqual(math.log(INF), INF) self.assertEqual(math.log(INF), INF)
self.assertTrue(math.isnan(math.log10(NAN))) self.assertTrue(math.isnan(math.log10(NAN)))
self.assertEqual(math.log10(IndexableFloatLike(100.0, 10**1000)), 2.0)
self.assertEqual(math.log10(IndexableFloatLike(OverflowError(), 10**1000)), 1000.0)
@support.bigmemtest(2**32, memuse=0.2) @support.bigmemtest(2**32, memuse=0.2)
def test_log_huge_integer(self, size): def test_log_huge_integer(self, size):
v = 1 << size v = 1 << size

View file

@ -0,0 +1,2 @@
:func:`math.log` now supports arbitrary large integer-like arguments in the
same way as arbitrary large integer arguments.

View file

@ -57,6 +57,7 @@ raised for division by zero and mod by zero.
#endif #endif
#include "Python.h" #include "Python.h"
#include "pycore_abstract.h" // _PyNumber_Index()
#include "pycore_bitutils.h" // _Py_bit_length() #include "pycore_bitutils.h" // _Py_bit_length()
#include "pycore_call.h" // _PyObject_CallNoArgs() #include "pycore_call.h" // _PyObject_CallNoArgs()
#include "pycore_import.h" // _PyImport_SetModuleString() #include "pycore_import.h" // _PyImport_SetModuleString()
@ -1577,44 +1578,63 @@ math_modf_impl(PyObject *module, double x)
However, intermediate overflow is possible for an int if the number of bits However, intermediate overflow is possible for an int if the number of bits
in that int is larger than PY_SSIZE_T_MAX. */ in that int is larger than PY_SSIZE_T_MAX. */
static PyObject*
loghelper_int(PyObject* arg, double (*func)(double))
{
/* If it is int, do it ourselves. */
double x, result;
int64_t e;
/* Negative or zero inputs give a ValueError. */
if (!_PyLong_IsPositive((PyLongObject *)arg)) {
PyErr_SetString(PyExc_ValueError,
"expected a positive input");
return NULL;
}
x = PyLong_AsDouble(arg);
if (x == -1.0 && PyErr_Occurred()) {
if (!PyErr_ExceptionMatches(PyExc_OverflowError))
return NULL;
/* Here the conversion to double overflowed, but it's possible
to compute the log anyway. Clear the exception and continue. */
PyErr_Clear();
x = _PyLong_Frexp((PyLongObject *)arg, &e);
assert(!PyErr_Occurred());
/* Value is ~= x * 2**e, so the log ~= log(x) + log(2) * e. */
result = fma(func(2.0), (double)e, func(x));
}
else
/* Successfully converted x to a double. */
result = func(x);
return PyFloat_FromDouble(result);
}
static PyObject* static PyObject*
loghelper(PyObject* arg, double (*func)(double)) loghelper(PyObject* arg, double (*func)(double))
{ {
/* If it is int, do it ourselves. */ /* If it is int, do it ourselves. */
if (PyLong_Check(arg)) { if (PyLong_Check(arg)) {
double x, result; return loghelper_int(arg, func);
int64_t e; }
/* Else let libm handle it by itself. */
/* Negative or zero inputs give a ValueError. */ PyObject *res = math_1(arg, func, 0, "expected a positive input, got %s");
if (!_PyLong_IsPositive((PyLongObject *)arg)) { if (res == NULL &&
/* The input can be an arbitrary large integer, so we PyErr_ExceptionMatches(PyExc_OverflowError) &&
don't include it's value in the error message. */ PyIndex_Check(arg))
PyErr_SetString(PyExc_ValueError, {
"expected a positive input"); /* Here the conversion to double overflowed, but it's possible
to compute the log anyway. Clear the exception, convert to
integer and continue. */
PyErr_Clear();
arg = _PyNumber_Index(arg);
if (arg == NULL) {
return NULL; return NULL;
} }
res = loghelper_int(arg, func);
x = PyLong_AsDouble(arg); Py_DECREF(arg);
if (x == -1.0 && PyErr_Occurred()) {
if (!PyErr_ExceptionMatches(PyExc_OverflowError))
return NULL;
/* Here the conversion to double overflowed, but it's possible
to compute the log anyway. Clear the exception and continue. */
PyErr_Clear();
x = _PyLong_Frexp((PyLongObject *)arg, &e);
assert(e >= 0);
assert(!PyErr_Occurred());
/* Value is ~= x * 2**e, so the log ~= log(x) + log(2) * e. */
result = fma(func(2.0), (double)e, func(x));
}
else
/* Successfully converted x to a double. */
result = func(x);
return PyFloat_FromDouble(result);
} }
return res;
/* Else let libm handle it by itself. */
return math_1(arg, func, 0, "expected a positive input, got %s");
} }