From 9d1766933467142edc5493128c315d770f0ac00d Mon Sep 17 00:00:00 2001 From: dot Date: Fri, 9 Oct 2026 20:01:58 +0900 Subject: [PATCH] Handle math.log bases without premature float conversion Use the natural logarithm path for both operands, evaluating x first. Preserve large integer bases and raise ZeroDivisionError for base one while retaining noninteger conversion errors. Split the Python 3.14-compatible behavior from #8954 without its additional __index__ fallback or changes to log2 and log10. Assisted-by: OpenAI Codex:model identifier unavailable --- crates/stdlib/src/math.rs | 53 ++++++++++++++------------- extra_tests/snippets/stdlib_math.py | 57 +++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 26 deletions(-) diff --git a/crates/stdlib/src/math.rs b/crates/stdlib/src/math.rs index 4b23b7b7b4a..3f5dab6b9b0 100644 --- a/crates/stdlib/src/math.rs +++ b/crates/stdlib/src/math.rs @@ -94,35 +94,36 @@ mod math { } #[pyfunction] - fn log(x: PyObjectRef, base: OptionalArg, vm: &VirtualMachine) -> PyResult { - let base = base.into_option().map(|v| v.into_float()); - // Check base first for proper error messages - if let Some(b) = base { - if b <= 0.0 { - return Err(vm.new_value_error(format!( - "expected a positive input, got {}", - super::float_repr(b) - ))); - } - if b == 1.0 { - return Err(vm.new_value_error("math domain error")); + fn log(x: PyObjectRef, base: OptionalArg, vm: &VirtualMachine) -> PyResult { + let natural_log = |x: &PyObject| { + // Handle BigInt specially for large values (only for actual int type, not float) + if let Some(i) = x.downcast_ref::() { + return pymath::math::log_bigint(i.as_bigint(), None).map_err(|err| match err { + pymath::Error::EDOM => vm.new_value_error("expected a positive input"), + _ => pymath_exception(err, vm), + }); } - } - // Handle BigInt specially for large values (only for actual int type, not float) - if let Some(i) = x.downcast_ref::() { - return pymath::math::log_bigint(i.as_bigint(), base).map_err(|err| match err { - pymath::Error::EDOM => vm.new_value_error("expected a positive input"), + let val = x.try_float(vm)?.to_f64(); + pymath::math::log(val, None).map_err(|err| match err { + pymath::Error::EDOM => vm.new_value_error(format!( + "expected a positive input, got {}", + super::float_repr(val) + )), _ => pymath_exception(err, vm), - }); + }) + }; + let numerator = natural_log(&x)?; + match base { + OptionalArg::Missing => Ok(numerator), + OptionalArg::Present(base) => { + let denominator = natural_log(&base)?; + if denominator == 0.0 { + Err(vm.new_zero_division_error("division by zero")) + } else { + Ok(numerator / denominator) + } + } } - let val = x.try_float(vm)?.to_f64(); - pymath::math::log(val, base).map_err(|err| match err { - pymath::Error::EDOM => vm.new_value_error(format!( - "expected a positive input, got {}", - super::float_repr(val) - )), - _ => pymath_exception(err, vm), - }) } #[pyfunction] diff --git a/extra_tests/snippets/stdlib_math.py b/extra_tests/snippets/stdlib_math.py index 00d6c5f3ab1..29824043aa2 100644 --- a/extra_tests/snippets/stdlib_math.py +++ b/extra_tests/snippets/stdlib_math.py @@ -17,6 +17,63 @@ # # assert math.log(1.1111) == math.log(Conversible()) +# A logarithm can fit in a float even when its integer base cannot. +assert math.isclose(math.log(2**4000, 2**2000), 2.0) +assert math.isclose(math.log(2.0, 2**2000), 1.0 / 2000) + +for value in (1, 8.0, INF, NAN): + assert_raises(ZeroDivisionError, math.log, value, 1) +assert_raises(TypeError, math.log, object(), 1) +assert_raises(ValueError, math.log, 8, -(2**2000)) + +log_conversions = [] + + +class LogFloat: + def __init__(self, name, value): + self.name = name + self.value = value + + def __float__(self): + log_conversions.append(self.name) + return self.value + + +assert math.isclose(math.log(LogFloat("x", 8.0), LogFloat("base", 2.0)), 3.0) +assert log_conversions == ["x", "base"] +log_conversions.clear() +assert_raises(ValueError, math.log, -1, LogFloat("base", 2.0)) +assert log_conversions == [] + + +class LogFloatOverflow: + def __float__(self): + raise OverflowError("float conversion failed") + + def __index__(self): + raise AssertionError("must not retry __index__ after __float__ fails") + + +assert_raises(OverflowError, math.log, LogFloatOverflow()) +assert_raises(OverflowError, math.log, 8, LogFloatOverflow()) + + +class LogIndexOnly: + def __index__(self): + return 2**2000 + + +assert_raises(OverflowError, math.log, LogIndexOnly()) +assert_raises(OverflowError, math.log, 8, LogIndexOnly()) + + +class LogInt(int): + def __float__(self): + raise AssertionError("integer logarithms use the native integer value") + + +assert math.isclose(math.log(LogInt(2**4000), LogInt(2**2000)), 2.0) + # roundings assert int.__trunc__ assert int.__floor__