Skip to content

Commit a444ffb

Browse files
authored
Convert math.ldexp()'s x like any float argument and read float subclasses directly (#9520)
* Convert the math.ldexp() mantissa like any float argument ldexp() accepted only a float or an int as x and raised TypeError for anything else, so an object with __float__ or __index__ was refused and an exception raised by __float__ was replaced. Bind x as ArgIntoFloat, which goes through the float protocol like the other math functions. Assisted-by: Claude Code:claude-opus-5-5 * Read a float subclass directly in ArgIntoFloat ArgIntoFloat read the value of an exact float but called __float__ on a float subclass, so a subclass overriding __float__ changed the result of math functions, struct packing, %-formatting and every other float argument. PyFloat_AsDouble, which ArgIntoFloat stands for, reads the value of any float instance and calls __float__ only for other types. Assisted-by: Claude Code:claude-opus-5-5
1 parent 95cde75 commit a444ffb

3 files changed

Lines changed: 42 additions & 11 deletions

File tree

‎crates/stdlib/src/math.rs‎

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,11 @@ use crate::vm::{VirtualMachine, builtins::PyBaseExceptionRef};
66
#[pymodule]
77
mod math {
88
use crate::vm::{
9-
AsObject, Py, PyObject, PyObjectRef, PyRef, PyResult, VirtualMachine,
9+
AsObject, Py, PyObject, PyObjectRef, PyResult, VirtualMachine,
1010
builtins::{
1111
PyFloat, PyInt, PyIntRef, PyModule, PyStrInterned, try_bigint_to_f64, try_f64_to_bigint,
1212
},
13-
function::{
14-
ArgIndex, ArgIntoFloat, ArgIterable, Either, NameCoordinates, OptionalArg, PosArgs,
15-
},
13+
function::{ArgIndex, ArgIntoFloat, ArgIterable, NameCoordinates, OptionalArg, PosArgs},
1614
identifier,
1715
};
1816
use malachite_bigint::BigInt;
@@ -457,7 +455,7 @@ mod math {
457455
#[derive(FromArgs)]
458456
struct LdexpArgs {
459457
#[pyarg(positional)]
460-
x: Either<PyRef<PyFloat>, PyIntRef>,
458+
x: ArgIntoFloat,
461459
// Refuses anything that is not an `int`, including objects with `__index__`.
462460
#[pyarg(positional, error_msg = "Expected an int as second argument to ldexp.")]
463461
i: PyIntRef,
@@ -466,11 +464,8 @@ mod math {
466464
#[pyfunction]
467465
fn ldexp(args: LdexpArgs, vm: &VirtualMachine) -> PyResult<f64> {
468466
let LdexpArgs { x, i } = args;
469-
let value = match x {
470-
Either::A(f) => f.to_f64(),
471-
Either::B(z) => try_bigint_to_f64(z.as_bigint(), vm)?,
472-
};
473-
pymath::math::ldexp_bigint(value, i.as_bigint()).map_err(|err| pymath_exception(err, vm))
467+
pymath::math::ldexp_bigint(x.into_float(), i.as_bigint())
468+
.map_err(|err| pymath_exception(err, vm))
474469
}
475470

476471
#[pyfunction]

‎crates/vm/src/function/number.rs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use super::argument::OptionalArg;
22
use crate::{
33
AsObject, Py, PyObjectRef, PyResult, TryFromObject, VirtualMachine,
4-
builtins::{PyInt, PyIntRef},
4+
builtins::{PyFloat, PyInt, PyIntRef},
55
};
66
use core::ops::Deref;
77
use malachite_bigint::BigInt;
@@ -88,6 +88,12 @@ impl From<ArgIntoFloat> for f64 {
8888
impl TryFromObject for ArgIntoFloat {
8989
// Equivalent to PyFloat_AsDouble.
9090
fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
91+
// A float subclass is read directly, without calling its __float__.
92+
if let Some(float) = obj.downcast_ref::<PyFloat>() {
93+
return Ok(Self {
94+
value: float.to_f64(),
95+
});
96+
}
9197
let value = obj.try_float(vm)?.to_f64();
9298
Ok(Self { value })
9399
}

‎extra_tests/snippets/stdlib_math.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,3 +541,33 @@ def assertAllNotClose(examples, *args, **kwargs):
541541
assert math.perm(5) == math.perm(5, None) == 120
542542
assert math.perm(2**64, 0) == 1
543543
assert math.comb(2**64, 2**64 - 1) == 2**64
544+
545+
546+
class LdexpFloat:
547+
def __float__(self):
548+
return 1.5
549+
550+
551+
class LdexpIndex:
552+
def __index__(self):
553+
return 3
554+
555+
556+
class LdexpRaisingFloat:
557+
def __float__(self):
558+
raise RuntimeError
559+
560+
561+
assert math.ldexp(LdexpFloat(), 1) == 3.0
562+
assert math.ldexp(LdexpIndex(), 1) == 6.0
563+
assert_raises(RuntimeError, math.ldexp, LdexpRaisingFloat(), 1)
564+
assert_raises(OverflowError, math.ldexp, 10**400, 1)
565+
566+
567+
class FloatSubclass(float):
568+
def __float__(self):
569+
return 9.0
570+
571+
572+
assert math.sqrt(FloatSubclass(4.0)) == 2.0
573+
assert math.ldexp(FloatSubclass(1.0), 1) == 2.0

0 commit comments

Comments
 (0)