Skip to content
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
103 changes: 50 additions & 53 deletions crates/stdlib/src/math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ mod math {
function::{ArgIndex, ArgIntoFloat, ArgIterable, NameCoordinates, OptionalArg, PosArgs},
identifier,
};
use itertools::Itertools;
use malachite_bigint::BigInt;
use num_traits::{Signed, ToPrimitive};

use super::{float_repr, pymath_exception};

Expand Down Expand Up @@ -150,8 +152,8 @@ mod math {
if let Some(i) = x.downcast_ref::<PyInt>() {
return integer_log(i);
}
let val = match x.try_float(vm) {
Ok(value) => value.to_f64(),
let val = match x.to_owned().try_into_value::<ArgIntoFloat>(vm) {
Ok(value) => value.into_float(),
Err(error) => {
if error.fast_isinstance(vm.ctx.exceptions.overflow_error)
&& let Some(index) = x.try_index_opt(vm)
Expand Down Expand Up @@ -402,64 +404,47 @@ mod math {
pymath::math::lgamma(x.into_float()).map_err(|err| pymath_exception(err, vm))
}

fn try_magic_method(
func_name: &'static PyStrInterned,
fn round_to_int(
x: &PyObject,
method: &'static PyStrInterned,
round: fn(f64) -> f64,
vm: &VirtualMachine,
value: &PyObject,
) -> PyResult {
let method = vm.get_method_or_type_error(value.to_owned(), func_name, || {
format!(
"type '{}' doesn't define '{}' method",
value.class().name(),
func_name.as_str(),
)
})?;
method.call((), vm)
// Exact floats skip special-method lookup. Other types may provide a
// result of any type, or fall back to the native float conversion.
let value = if let Some(float) = x.downcast_ref_if_exact::<PyFloat>(vm) {
float.to_f64()
} else {
if let Some(result) = vm.call_special_method_opt(x, method, ())? {
return Ok(result);
}
x.to_owned()
.try_into_value::<ArgIntoFloat>(vm)?
.into_float()
};
let value = try_f64_to_bigint(round(value), vm)?;
Ok(vm.ctx.new_int(value).into())
}

#[pyfunction]
fn trunc(x: PyObjectRef, vm: &VirtualMachine) -> PyResult {
try_magic_method(identifier!(vm, __trunc__), vm, &x)
vm.call_special_method_opt(&x, identifier!(vm, __trunc__), ())?
.ok_or_else(|| {
let class = x.class();
let name = class.slot_name();
let name = &name[..name.floor_char_boundary(100)];
vm.new_type_error(format!("type {name} doesn't define __trunc__ method"))
})
}

#[pyfunction]
fn ceil(x: PyObjectRef, vm: &VirtualMachine) -> PyResult {
// `math_ceil`: an exact float skips the `__ceil__` lookup.
if let Some(f) = x.downcast_ref_if_exact::<PyFloat>(vm) {
let v = try_f64_to_bigint(f.to_f64().ceil(), vm)?;
return Ok(vm.ctx.new_int(v).into());
}
// Only call __ceil__ if the class defines it - if it exists but is not callable,
// the error should be propagated (not fall back to float conversion)
if x.class().has_attr(identifier!(vm, __ceil__)) {
return try_magic_method(identifier!(vm, __ceil__), vm, &x);
}
// __ceil__ not defined - fall back to float conversion
if let Some(v) = x.try_float_opt(vm) {
let v = try_f64_to_bigint(v?.to_f64().ceil(), vm)?;
return Ok(vm.ctx.new_int(v).into());
}
Err(vm.new_type_error(format!("must be real number, not {}", x.class().name())))
round_to_int(&x, identifier!(vm, __ceil__), f64::ceil, vm)
}

#[pyfunction]
fn floor(x: PyObjectRef, vm: &VirtualMachine) -> PyResult {
// `math_floor`: an exact float skips the `__floor__` lookup.
if let Some(f) = x.downcast_ref_if_exact::<PyFloat>(vm) {
let v = try_f64_to_bigint(f.to_f64().floor(), vm)?;
return Ok(vm.ctx.new_int(v).into());
}
// Only call __floor__ if the class defines it - if it exists but is not callable,
// the error should be propagated (not fall back to float conversion)
if x.class().has_attr(identifier!(vm, __floor__)) {
return try_magic_method(identifier!(vm, __floor__), vm, &x);
}
// __floor__ not defined - fall back to float conversion
if let Some(v) = x.try_float_opt(vm) {
let v = try_f64_to_bigint(v?.to_f64().floor(), vm)?;
return Ok(vm.ctx.new_int(v).into());
}
Err(vm.new_type_error(format!("must be real number, not {}", x.class().name())))
round_to_int(&x, identifier!(vm, __floor__), f64::floor, vm)
}

#[pyfunction]
Expand Down Expand Up @@ -489,10 +474,20 @@ mod math {
}

#[pyfunction]
fn fsum(seq: ArgIterable<ArgIntoFloat>, vm: &VirtualMachine) -> PyResult<f64> {
let values: Result<Vec<f64>, _> =
seq.iter(vm)?.map(|r| r.map(|v| v.into_float())).collect();
pymath::math::fsum(values?).map_err(|err| pymath_exception(err, vm))
fn fsum(seq: ArgIterable<ArgIntoFloat>, vm: &VirtualMachine) -> PyResult {
// Stream conversions so an intermediate overflow does not advance the
// Python iterator or invoke __float__ on any later summands.
let mut values = seq
.iter(vm)?
.map(|value| value.map(ArgIntoFloat::into_float));
values
.by_ref()
.process_results(|values| pymath::math::fsum(values))?
.map(|value| vm.ctx.new_float(value).into())
.map_err(|err| match err {
pymath::Error::ERANGE => vm.new_overflow_error("intermediate overflow in fsum"),
pymath::Error::EDOM => vm.new_value_error("-inf + inf in fsum"),
})
}

#[pyfunction]
Expand All @@ -518,11 +513,13 @@ mod math {

let steps = match arg.steps {
Some(steps) => {
let steps: i64 = steps.into_int_ref().try_to_primitive(vm)?;
if steps < 0 {
let steps = steps.as_ref().as_bigint();
if steps.is_negative() {
return Err(vm.new_value_error("steps must be a non-negative integer"));
}
Some(steps as u64)
// Every finite IEEE-754 path fits in u64 steps. Larger indices
// reach the destination, just like CPython's saturated count.
Some(steps.to_u64().unwrap_or(u64::MAX))
}
None => None,
};
Expand Down
136 changes: 69 additions & 67 deletions crates/vm/src/builtins/complex.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use super::{PyInt, PyStr, PyType, PyTypeRef, float};
use super::{PyFloat, PyInt, PyStr, PyType, PyTypeRef, float};
use crate::{
AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject,
VirtualMachine,
builtins::PyUtf8StrRef,
class::{PyClassDef, PyClassImpl},
common::{float_ops, wtf8::Wtf8Buf},
convert::{ToPyObject, ToPyResult},
function::{FuncArgs, OptionalArg, PyComparisonValue},
function::{ArgIntoComplex, ArgIntoFloat, FuncArgs, OptionalArg, PyComparisonValue},
protocol::PyNumberMethods,
stdlib::_warnings,
types::{AsNumber, Callable, Comparable, Constructor, Hashable, PyComparisonOp, Representable},
Expand Down Expand Up @@ -92,23 +93,26 @@ impl From<Complex64> for PyComplex {
}

impl PyObjectRef {
/// Tries converting a python object into a complex, returns an option of whether the complex
/// and whether the object was a complex originally or coerced into one
/// Convert a constructor argument, preferring an overridden `__complex__`.
/// The flag records whether the result came from a complex value or method,
/// rather than a real-number conversion.
pub fn try_complex(&self, vm: &VirtualMachine) -> PyResult<Option<(Complex64, bool)>> {
if let Some(complex) = self.downcast_ref_if_exact::<PyComplex>(vm) {
return Ok(Some((complex.as_complex(), true)));
}
if let Some(value) = self.try_complex_special_method(vm)? {
return Ok(Some((value.as_complex(), true)));
}
// `complex` does not have a `__complex__` by default, so subclasses might not either,
// use the actual stored value in this case
// A complex subtype whose special method is absent still has a value.
if let Some(complex) = self.downcast_ref::<PyComplex>() {
return Ok(Some((complex.as_complex(), true)));
}

if let Some(float) = self.try_float_opt(vm) {
return Ok(Some((Complex64::new(float?.to_f64(), 0.0), false)));
// Unlike the two-component constructor, the single-argument form uses
// PyFloat_AsDouble: read float subclasses without invoking __float__.
if self.downcastable::<PyFloat>() || has_real_number_conversion(self) {
let real = ArgIntoFloat::try_from_object(vm, self.clone())?.into_float();
return Ok(Some((Complex64::new(real, 0.0), false)));
}

Ok(None)
Expand All @@ -118,36 +122,32 @@ impl PyObjectRef {
&self,
vm: &VirtualMachine,
) -> PyResult<Option<PyRef<PyComplex>>> {
if let Some(method) = vm.get_method(self.clone(), identifier!(vm, __complex__)) {
let result = method?.call((), vm)?;

let ret_class = result.class().to_owned();
if let Some(ret) = result.downcast_ref::<PyComplex>()
&& !result.class().is(vm.ctx.types.complex_type)
{
_warnings::warn(
vm.ctx.exceptions.deprecation_warning,
format!(
"__complex__ returned non-complex (type {ret_class}). \
The ability to return an instance of a strict subclass of complex \
is deprecated, and may be removed in a future version of Python."
),
1,
vm,
)?;

return Ok(Some(ret.to_owned()));
}

return match result.downcast_ref::<PyComplex>() {
Some(complex_obj) => Ok(Some(complex_obj.to_owned())),
None => Err(vm.new_type_error(format!(
"__complex__ returned non-complex (type '{}')",
result.class().name()
))),
};
let Some(method) = vm.get_method(self.clone(), identifier!(vm, __complex__)) else {
return Ok(None);
};
let result = method?.call((), vm)?;
if let Some(complex) = result.downcast_ref_if_exact::<PyComplex>(vm) {
return Ok(Some(complex.to_owned()));
}
Ok(None)

let message = wtf8_concat!(
self.class().fully_qualified_name(vm)?,
".__complex__() must return a complex, not ",
result.class().fully_qualified_name(vm)?
);
let Some(complex) = result.downcast_ref::<PyComplex>() else {
return Err(vm.new_type_error(message));
};
_warnings::warn(
vm.ctx.exceptions.deprecation_warning,
wtf8_concat!(
message,
". The ability to return an instance of a strict subclass of complex is deprecated, and may be removed in a future version of Python."
),
1,
vm,
)?;
Ok(Some(complex.to_owned()))
}
}

Expand Down Expand Up @@ -385,9 +385,20 @@ fn powc(a: Complex64, exp: Complex64) -> Complex64 {
Complex64::new(len * phase.cos(), len * phase.sin())
}

/// Whether an underscore sits where a numeric literal does not allow one:
/// they only ever join two digits.
fn has_misplaced_underscore(bytes: &[u8]) -> bool {
/// Whether underscore validation rejects the literal, including an embedded
/// NUL when an underscore occurs before it.
fn invalid_underscored_literal(bytes: &[u8]) -> bool {
// CPython only enters underscore validation if one occurs before a NUL.
let end = bytes
.iter()
.position(|&byte| byte == 0)
.unwrap_or(bytes.len());
if !bytes[..end].contains(&b'_') {
return false;
}
if end != bytes.len() {
return true;
}
let mut prev = b'\0';
for &byte in bytes {
if byte == b'_' {
Expand Down Expand Up @@ -523,27 +534,24 @@ fn has_real_number_conversion(value: &PyObject) -> bool {
impl PyComplex {
fn from_single_argument(value: &PyObjectRef, vm: &VirtualMachine) -> PyResult<Self> {
if let Some(string) = value.downcast_ref::<PyStr>() {
if has_misplaced_underscore(string.as_wtf8().as_bytes()) {
let literal = crate::protocol::numeric_literal_from_str(string);
if invalid_underscored_literal(literal.as_bytes()) {
let repr = value.repr(vm)?;
return Err(
vm.new_value_error(format!("could not convert string to complex: {repr}"))
);
}
let (re, im) = rustpython_literal::complex::parse_str(
&crate::protocol::numeric_literal_from_str(string),
)
.ok_or_else(|| vm.new_value_error("complex() arg is a malformed string"))?;
let (re, im) = rustpython_literal::complex::parse_str(&literal)
.ok_or_else(|| vm.new_value_error("complex() arg is a malformed string"))?;
return Ok(Self::from(Complex64 { re, im }));
}
value
.try_complex(vm)?
.map(|(value, _)| Self::from(value))
.ok_or_else(|| {
vm.new_type_error(format!(
"complex() argument must be a string or a number, not {}",
value.class().slot_name()
))
})
match value.try_complex(vm)? {
Some((value, _)) => Ok(Self::from(value)),
None => Err(vm.new_type_error(wtf8_concat!(
"complex() argument must be a string or a number, not ",
value.class().fully_qualified_name(vm)?
))),
}
}

#[deprecated(note = "use PyComplex::from(...).into_ref() instead")]
Expand Down Expand Up @@ -657,29 +665,23 @@ impl Py<PyComplex> {
if number.class().is(vm.ctx.types.complex_type) && cls.is(vm.ctx.types.complex_type) {
return Ok(number);
}
let value = number
.try_complex(vm)?
.ok_or_else(|| {
vm.new_type_error(format!(
"must be real number, not {}",
number.class().name()
))
})?
.0;
let value = ArgIntoComplex::try_from_object(vm, number.clone())?.into_complex();
let result = vm.ctx.new_complex(value);
if cls.is(vm.ctx.types.complex_type) {
let result = if cls.is(vm.ctx.types.complex_type) {
Ok(result.into())
} else {
PyType::call(&cls, vec![result.into()].into(), vm)
}
};
drop(number);
result
}
}

#[pyclass]
impl PyRef<PyComplex> {
#[pymethod]
fn __complex__(self, vm: &VirtualMachine) -> Self {
if self.is(vm.ctx.types.complex_type) {
if self.class().is(vm.ctx.types.complex_type) {
self
} else {
PyComplex::from(self.as_complex()).into_ref(&vm.ctx)
Expand Down
Loading
Loading