diff --git a/Lib/test/test_float.py b/Lib/test/test_float.py index 39208dab9a..bc3d5bdc7c 100644 --- a/Lib/test/test_float.py +++ b/Lib/test/test_float.py @@ -177,8 +177,6 @@ def test_float_with_comma(self): self.assertEqual(float(" 25.e-1 "), 2.5) self.assertAlmostEqual(float(" .25e-1 "), .025) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_floatconversion(self): # Make sure that calls to __float__() work properly class Foo1(object): diff --git a/Lib/test/test_index.py b/Lib/test/test_index.py index 1fac132595..cbdc56c801 100644 --- a/Lib/test/test_index.py +++ b/Lib/test/test_index.py @@ -71,8 +71,6 @@ def __index__(self): self.assertIs(type(direct_index), int) #self.assertIs(type(operator_index), int) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_index_returns_int_subclass(self): class BadInt: def __index__(self): diff --git a/Lib/test/test_int.py b/Lib/test/test_int.py index 87cde63c7c..7ac83288f4 100644 --- a/Lib/test/test_int.py +++ b/Lib/test/test_int.py @@ -458,8 +458,6 @@ def __int__(self): self.assertEqual(my_int, 7) self.assertRaises(TypeError, int, my_int) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_int_returns_int_subclass(self): class BadIndex: def __index__(self): diff --git a/stdlib/src/math.rs b/stdlib/src/math.rs index e118ce77c7..08f450323e 100644 --- a/stdlib/src/math.rs +++ b/stdlib/src/math.rs @@ -454,8 +454,8 @@ mod math { fn ceil(x: PyObjectRef, vm: &VirtualMachine) -> PyResult { let result_or_err = try_magic_method(identifier!(vm, __ceil__), vm, &x); if result_or_err.is_err() { - if let Ok(Some(v)) = x.try_to_f64(vm) { - let v = try_f64_to_bigint(v.ceil(), vm)?; + if let Ok(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()); } } @@ -466,8 +466,8 @@ mod math { fn floor(x: PyObjectRef, vm: &VirtualMachine) -> PyResult { let result_or_err = try_magic_method(identifier!(vm, __floor__), vm, &x); if result_or_err.is_err() { - if let Ok(Some(v)) = x.try_to_f64(vm) { - let v = try_f64_to_bigint(v.floor(), vm)?; + if let Ok(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()); } } diff --git a/vm/src/buffer.rs b/vm/src/buffer.rs index 046162fb14..f2e05e828c 100644 --- a/vm/src/buffer.rs +++ b/vm/src/buffer.rs @@ -1,8 +1,8 @@ use crate::{ - builtins::{float, PyBaseExceptionRef, PyBytesRef, PyTuple, PyTupleRef, PyTypeRef}, + builtins::{PyBaseExceptionRef, PyBytesRef, PyTuple, PyTupleRef, PyTypeRef}, common::{static_cell, str::wchar_t}, convert::ToPyObject, - function::{ArgBytesLike, ArgIntoBool}, + function::{ArgBytesLike, ArgIntoBool, ArgIntoFloat}, PyObjectRef, PyResult, TryFromObject, VirtualMachine, }; use half::f16; @@ -521,7 +521,7 @@ macro_rules! make_pack_float { arg: PyObjectRef, data: &mut [u8], ) -> PyResult<()> { - let f = float::try_float(&arg, vm)? as $T; + let f = *ArgIntoFloat::try_from_object(vm, arg)? as $T; f.to_bits().pack_int::(data); Ok(()) } @@ -539,7 +539,7 @@ make_pack_float!(f64); impl Packable for f16 { fn pack(vm: &VirtualMachine, arg: PyObjectRef, data: &mut [u8]) -> PyResult<()> { - let f_64 = float::try_float(&arg, vm)?; + let f_64 = *ArgIntoFloat::try_from_object(vm, arg)?; let f_16 = f16::from_f64(f_64); if f_16.is_infinite() != f_64.is_infinite() { return Err(vm.new_overflow_error("float too large to pack with e format".to_owned())); diff --git a/vm/src/builtins/complex.rs b/vm/src/builtins/complex.rs index 2cfc84c7bb..9e4f5af904 100644 --- a/vm/src/builtins/complex.rs +++ b/vm/src/builtins/complex.rs @@ -65,8 +65,8 @@ impl PyObjectRef { if let Some(complex) = self.payload_if_subclass::(vm) { return Ok(Some((complex.value, true))); } - if let Some(float) = self.try_to_f64(vm)? { - return Ok(Some((Complex64::new(float, 0.0), false))); + if let Some(float) = self.try_float_opt(vm)? { + return Ok(Some((Complex64::new(float.to_f64(), 0.0), false))); } Ok(None) } diff --git a/vm/src/builtins/float.rs b/vm/src/builtins/float.rs index 5453971cbb..d83439b5eb 100644 --- a/vm/src/builtins/float.rs +++ b/vm/src/builtins/float.rs @@ -1,18 +1,18 @@ use super::{ try_bigint_to_f64, PyByteArray, PyBytes, PyInt, PyIntRef, PyStr, PyStrRef, PyType, PyTypeRef, }; -use crate::common::{float_ops, hash}; use crate::{ class::PyClassImpl, - convert::ToPyObject, + common::{float_ops, hash}, + convert::{ToPyObject, ToPyResult}, format::FormatSpec, function::{ ArgBytesLike, OptionalArg, OptionalOption, PyArithmeticValue::{self, *}, PyComparisonValue, }, - identifier, - types::{Comparable, Constructor, Hashable, PyComparisonOp}, + protocol::{PyNumber, PyNumberMethods}, + types::{AsNumber, Comparable, Constructor, Hashable, PyComparisonOp}, AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromBorrowedObject, TryFromObject, VirtualMachine, }; @@ -58,32 +58,13 @@ impl From for PyFloat { } impl PyObject { - pub fn try_to_f64(&self, vm: &VirtualMachine) -> PyResult> { - if let Some(float) = self.payload_if_exact::(vm) { - return Ok(Some(float.value)); - } - if let Some(method) = vm.get_method(self.to_owned(), identifier!(vm, __float__)) { - let result = vm.invoke(&method?, ())?; - // TODO: returning strict subclasses of float in __float__ is deprecated - return match result.payload::() { - Some(float_obj) => Ok(Some(float_obj.value)), - None => Err(vm.new_type_error(format!( - "__float__ returned non-float (type '{}')", - result.class().name() - ))), - }; - } - if let Some(r) = vm.to_index_opt(self.to_owned()).transpose()? { - return Ok(Some(try_bigint_to_f64(r.as_bigint(), vm)?)); - } - Ok(None) + pub fn try_float_opt(&self, vm: &VirtualMachine) -> PyResult>> { + PyNumber::new(self, vm).float_opt(vm) } -} -pub fn try_float(obj: &PyObject, vm: &VirtualMachine) -> PyResult { - obj.try_to_f64(vm)?.ok_or_else(|| { - vm.new_type_error(format!("must be real number, not {}", obj.class().name())) - }) + pub fn try_float(&self, vm: &VirtualMachine) -> PyResult> { + PyNumber::new(self, vm).float(vm) + } } pub(crate) fn to_op_float(obj: &PyObject, vm: &VirtualMachine) -> PyResult> { @@ -170,17 +151,8 @@ impl Constructor for PyFloat { let float_val = match arg { OptionalArg::Missing => 0.0, OptionalArg::Present(val) => { - let val = if cls.is(vm.ctx.types.float_type) { - match val.downcast_exact::(vm) { - Ok(f) => return Ok(f.into()), - Err(val) => val, - } - } else { - val - }; - - if let Some(f) = val.try_to_f64(vm)? { - f + if let Some(f) = val.try_float_opt(vm)? { + f.value } else { float_from_string(val, vm)? } @@ -220,7 +192,7 @@ fn float_from_string(val: PyObjectRef, vm: &VirtualMachine) -> PyResult { }) } -#[pyimpl(flags(BASETYPE), with(Comparable, Hashable, Constructor))] +#[pyimpl(flags(BASETYPE), with(Comparable, Hashable, Constructor, AsNumber))] impl PyFloat { #[pymethod(magic)] fn format(&self, spec: PyStrRef, vm: &VirtualMachine) -> PyResult { @@ -562,6 +534,78 @@ impl Hashable for PyFloat { } } +impl AsNumber for PyFloat { + const AS_NUMBER: PyNumberMethods = PyNumberMethods { + add: Some(|number, other, vm| Self::number_float_op(number, other, |a, b| a + b, vm)), + subtract: Some(|number, other, vm| Self::number_float_op(number, other, |a, b| a - b, vm)), + multiply: Some(|number, other, vm| Self::number_float_op(number, other, |a, b| a * b, vm)), + remainder: Some(|number, other, vm| Self::number_general_op(number, other, inner_mod, vm)), + divmod: Some(|number, other, vm| Self::number_general_op(number, other, inner_divmod, vm)), + power: Some(|number, other, vm| Self::number_general_op(number, other, float_pow, vm)), + negative: Some(|number, vm| { + let value = Self::number_downcast(number).value; + (-value).to_pyresult(vm) + }), + positive: Some(|number, vm| Self::number_float(number, vm).to_pyresult(vm)), + absolute: Some(|number, vm| { + let value = Self::number_downcast(number).value; + value.abs().to_pyresult(vm) + }), + boolean: Some(|number, _vm| Ok(Self::number_downcast(number).value.is_zero())), + int: Some(|number, vm| { + let value = Self::number_downcast(number).value; + try_to_bigint(value, vm).map(|x| vm.ctx.new_int(x)) + }), + float: Some(|number, vm| Ok(Self::number_float(number, vm))), + floor_divide: Some(|number, other, vm| { + Self::number_general_op(number, other, inner_floordiv, vm) + }), + true_divide: Some(|number, other, vm| { + Self::number_general_op(number, other, inner_div, vm) + }), + ..PyNumberMethods::NOT_IMPLEMENTED + }; +} + +impl PyFloat { + fn number_general_op( + number: &PyNumber, + other: &PyObject, + op: F, + vm: &VirtualMachine, + ) -> PyResult + where + F: FnOnce(f64, f64, &VirtualMachine) -> R, + R: ToPyResult, + { + if let (Some(a), Some(b)) = (to_op_float(number.obj, vm)?, to_op_float(other, vm)?) { + op(a, b, vm).to_pyresult(vm) + } else { + Ok(vm.ctx.not_implemented()) + } + } + + fn number_float_op( + number: &PyNumber, + other: &PyObject, + op: F, + vm: &VirtualMachine, + ) -> PyResult + where + F: FnOnce(f64, f64) -> f64, + { + Self::number_general_op(number, other, |a, b, _vm| op(a, b), vm) + } + + fn number_float(number: &PyNumber, vm: &VirtualMachine) -> PyRef { + if let Some(zelf) = number.obj.downcast_ref_if_exact::(vm) { + zelf.to_owned() + } else { + vm.ctx.new_float(Self::number_downcast(number).value) + } + } +} + // Retrieve inner float value: pub(crate) fn get_value(obj: &PyObject) -> f64 { obj.payload::().unwrap().value diff --git a/vm/src/builtins/int.rs b/vm/src/builtins/int.rs index ecca0b544e..9a1ad9cfab 100644 --- a/vm/src/builtins/int.rs +++ b/vm/src/builtins/int.rs @@ -9,7 +9,8 @@ use crate::{ ArgByteOrder, ArgIntoBool, OptionalArg, OptionalOption, PyArithmeticValue, PyComparisonValue, }, - types::{Comparable, Constructor, Hashable, PyComparisonOp}, + protocol::{PyNumber, PyNumberMethods}, + types::{AsNumber, Comparable, Constructor, Hashable, PyComparisonOp}, AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromBorrowedObject, VirtualMachine, }; @@ -17,7 +18,8 @@ use bstr::ByteSlice; use num_bigint::{BigInt, BigUint, Sign}; use num_integer::Integer; use num_traits::{One, Pow, PrimInt, Signed, ToPrimitive, Zero}; -use std::fmt; +use std::ops::Neg; +use std::{fmt, ops::Not}; /// int(x=0) -> integer /// int(x, base=10) -> integer @@ -263,7 +265,9 @@ impl Constructor for PyInt { val }; - try_int(&val, vm) + PyNumber::new(val.as_ref(), vm) + .int(vm) + .map(|x| x.as_bigint().clone()) } } else if let OptionalArg::Present(_) = options.base { Err(vm.new_type_error("int() missing string argument".to_owned())) @@ -346,7 +350,7 @@ impl PyInt { } } -#[pyimpl(flags(BASETYPE), with(Comparable, Hashable, Constructor))] +#[pyimpl(flags(BASETYPE), with(Comparable, Hashable, Constructor, AsNumber))] impl PyInt { #[pymethod(name = "__radd__")] #[pymethod(magic)] @@ -751,6 +755,74 @@ impl Hashable for PyInt { } } +impl AsNumber for PyInt { + const AS_NUMBER: PyNumberMethods = PyNumberMethods { + add: Some(|number, other, vm| Self::number_int_op(number, other, |a, b| a + b, vm)), + subtract: Some(|number, other, vm| Self::number_int_op(number, other, |a, b| a - b, vm)), + multiply: Some(|number, other, vm| Self::number_int_op(number, other, |a, b| a * b, vm)), + remainder: Some(|number, other, vm| Self::number_general_op(number, other, inner_mod, vm)), + divmod: Some(|number, other, vm| Self::number_general_op(number, other, inner_divmod, vm)), + power: Some(|number, other, vm| Self::number_general_op(number, other, inner_pow, vm)), + negative: Some(|number, vm| (&Self::number_downcast(number).value).neg().to_pyresult(vm)), + positive: Some(|number, vm| Ok(Self::number_int(number, vm).into())), + absolute: Some(|number, vm| Self::number_downcast(number).value.abs().to_pyresult(vm)), + boolean: Some(|number, _vm| Ok(Self::number_downcast(number).value.is_zero())), + invert: Some(|number, vm| (&Self::number_downcast(number).value).not().to_pyresult(vm)), + lshift: Some(|number, other, vm| Self::number_general_op(number, other, inner_lshift, vm)), + rshift: Some(|number, other, vm| Self::number_general_op(number, other, inner_rshift, vm)), + and: Some(|number, other, vm| Self::number_int_op(number, other, |a, b| a & b, vm)), + xor: Some(|number, other, vm| Self::number_int_op(number, other, |a, b| a ^ b, vm)), + or: Some(|number, other, vm| Self::number_int_op(number, other, |a, b| a | b, vm)), + int: Some(|number, other| Ok(Self::number_int(number, other))), + float: Some(|number, vm| { + let zelf = Self::number_downcast(number); + try_to_float(&zelf.value, vm).map(|x| vm.ctx.new_float(x)) + }), + floor_divide: Some(|number, other, vm| { + Self::number_general_op(number, other, inner_floordiv, vm) + }), + true_divide: Some(|number, other, vm| { + Self::number_general_op(number, other, inner_truediv, vm) + }), + index: Some(|number, vm| Ok(Self::number_int(number, vm))), + ..PyNumberMethods::NOT_IMPLEMENTED + }; +} + +impl PyInt { + fn number_general_op( + number: &PyNumber, + other: &PyObject, + op: F, + vm: &VirtualMachine, + ) -> PyResult + where + F: FnOnce(&BigInt, &BigInt, &VirtualMachine) -> PyResult, + { + if let (Some(a), Some(b)) = (number.obj.payload::(), other.payload::()) { + op(&a.value, &b.value, vm) + } else { + Ok(vm.ctx.not_implemented()) + } + } + + fn number_int_op(number: &PyNumber, other: &PyObject, op: F, vm: &VirtualMachine) -> PyResult + where + F: FnOnce(&BigInt, &BigInt) -> BigInt, + { + Self::number_general_op(number, other, |a, b, _vm| op(a, b).to_pyresult(vm), vm) + } + + fn number_int(number: &PyNumber, vm: &VirtualMachine) -> PyIntRef { + if let Some(zelf) = number.obj.downcast_ref_if_exact::(vm) { + zelf.to_owned() + } else { + let zelf = Self::number_downcast(number); + vm.ctx.new_int(zelf.value.clone()) + } + } +} + #[derive(FromArgs)] pub struct IntOptions { #[pyarg(positional, optional)] @@ -807,7 +879,7 @@ fn try_int_radix(obj: &PyObject, base: u32, vm: &VirtualMachine) -> PyResult Option { +pub(crate) fn bytes_to_int(lit: &[u8], mut base: u32) -> Option { // split sign let mut lit = lit.trim(); let sign = match lit.first()? { @@ -926,65 +998,6 @@ fn i2f(int: &BigInt) -> Option { int.to_f64().filter(|f| f.is_finite()) } -pub(crate) fn try_int(obj: &PyObject, vm: &VirtualMachine) -> PyResult { - fn try_convert(obj: &PyObject, lit: &[u8], vm: &VirtualMachine) -> PyResult { - let base = 10; - match bytes_to_int(lit, base) { - Some(i) => Ok(i), - None => Err(vm.new_value_error(format!( - "invalid literal for int() with base {}: {}", - base, - obj.repr(vm)?, - ))), - } - } - - // test for strings and bytes - if let Some(s) = obj.downcast_ref::() { - return try_convert(obj, s.as_str().as_bytes(), vm); - } - if let Ok(r) = obj.try_bytes_like(vm, |x| try_convert(obj, x, vm)) { - return r; - } - // strict `int` check - if let Some(int) = obj.payload_if_exact::(vm) { - return Ok(int.as_bigint().clone()); - } - // call __int__, then __index__, then __trunc__ (converting the __trunc__ result via __index__ if needed) - // TODO: using __int__ is deprecated and removed in Python 3.10 - if let Some(method) = vm.get_method(obj.to_owned(), identifier!(vm, __int__)) { - let result = vm.invoke(&method?, ())?; - return match result.payload::() { - Some(int_obj) => Ok(int_obj.as_bigint().clone()), - None => Err(vm.new_type_error(format!( - "__int__ returned non-int (type '{}')", - result.class().name() - ))), - }; - } - // TODO: returning strict subclasses of int in __index__ is deprecated - if let Some(r) = vm.to_index_opt(obj.to_owned()).transpose()? { - return Ok(r.as_bigint().clone()); - } - if let Some(method) = vm.get_method(obj.to_owned(), identifier!(vm, __trunc__)) { - let result = vm.invoke(&method?, ())?; - return vm - .to_index_opt(result.clone()) - .unwrap_or_else(|| { - Err(vm.new_type_error(format!( - "__trunc__ returned non-Integral (type {})", - result.class().name() - ))) - }) - .map(|int_obj| int_obj.as_bigint().clone()); - } - - Err(vm.new_type_error(format!( - "int() argument must be a string, a bytes-like object or a number, not '{}'", - obj.class().name() - ))) -} - pub(crate) fn init(context: &Context) { PyInt::extend_class(context, context.types.int_type); } diff --git a/vm/src/function/number.rs b/vm/src/function/number.rs index 96ff57827b..4e8219c342 100644 --- a/vm/src/function/number.rs +++ b/vm/src/function/number.rs @@ -1,4 +1,4 @@ -use crate::{AsObject, PyObjectRef, PyResult, TryFromObject, VirtualMachine}; +use crate::{protocol::PyNumber, AsObject, PyObjectRef, PyResult, TryFromObject, VirtualMachine}; use num_complex::Complex64; use std::ops::Deref; @@ -82,9 +82,7 @@ impl Deref for ArgIntoFloat { impl TryFromObject for ArgIntoFloat { // Equivalent to PyFloat_AsDouble. fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { - let value = obj.try_to_f64(vm)?.ok_or_else(|| { - vm.new_type_error(format!("must be real number, not {}", obj.class().name())) - })?; + let value = PyNumber::new(obj.as_ref(), vm).float(vm)?.to_f64(); Ok(ArgIntoFloat { value }) } } diff --git a/vm/src/protocol/mod.rs b/vm/src/protocol/mod.rs index 9415bb455e..dae492628e 100644 --- a/vm/src/protocol/mod.rs +++ b/vm/src/protocol/mod.rs @@ -1,10 +1,12 @@ mod buffer; mod iter; mod mapping; +mod number; mod object; mod sequence; pub use buffer::{BufferDescriptor, BufferMethods, BufferResizeGuard, PyBuffer, VecBuffer}; pub use iter::{PyIter, PyIterIter, PyIterReturn}; pub use mapping::{PyMapping, PyMappingMethods}; +pub use number::{PyNumber, PyNumberMethods}; pub use sequence::{PySequence, PySequenceMethods}; diff --git a/vm/src/protocol/number.rs b/vm/src/protocol/number.rs new file mode 100644 index 0000000000..f2420f709b --- /dev/null +++ b/vm/src/protocol/number.rs @@ -0,0 +1,281 @@ +use crate::{ + builtins::{int, PyByteArray, PyBytes, PyComplex, PyFloat, PyInt, PyIntRef, PyStr}, + function::ArgBytesLike, + stdlib::warnings, + AsObject, PyObject, PyPayload, PyRef, PyResult, TryFromBorrowedObject, VirtualMachine, +}; + +#[allow(clippy::type_complexity)] +#[derive(Clone)] +pub struct PyNumberMethods { + /* Number implementations must check *both* + arguments for proper type and implement the necessary conversions + in the slot functions themselves. */ + pub add: Option PyResult>, + pub subtract: Option PyResult>, + pub multiply: Option PyResult>, + pub remainder: Option PyResult>, + pub divmod: Option PyResult>, + pub power: Option PyResult>, + pub negative: Option PyResult>, + pub positive: Option PyResult>, + pub absolute: Option PyResult>, + pub boolean: Option PyResult>, + pub invert: Option PyResult>, + pub lshift: Option PyResult>, + pub rshift: Option PyResult>, + pub and: Option PyResult>, + pub xor: Option PyResult>, + pub or: Option PyResult>, + pub int: Option PyResult>, + pub float: Option PyResult>>, + + pub inplace_add: Option PyResult>, + pub inplace_subtract: Option PyResult>, + pub inplace_multiply: Option PyResult>, + pub inplace_remainder: Option PyResult>, + pub inplace_divmod: Option PyResult>, + pub inplace_power: Option PyResult>, + pub inplace_lshift: Option PyResult>, + pub inplace_rshift: Option PyResult>, + pub inplace_and: Option PyResult>, + pub inplace_xor: Option PyResult>, + pub inplace_or: Option PyResult>, + + pub floor_divide: Option PyResult>, + pub true_divide: Option PyResult>, + pub inplace_floor_divide: Option PyResult>, + pub inplace_true_divide: Option PyResult>, + + pub index: Option PyResult>, + + pub matrix_multiply: Option PyResult>, + pub inplace_matrix_multiply: Option PyResult>, +} + +impl PyNumberMethods { + pub const NOT_IMPLEMENTED: PyNumberMethods = PyNumberMethods { + add: None, + subtract: None, + multiply: None, + remainder: None, + divmod: None, + power: None, + negative: None, + positive: None, + absolute: None, + boolean: None, + invert: None, + lshift: None, + rshift: None, + and: None, + xor: None, + or: None, + int: None, + float: None, + inplace_add: None, + inplace_subtract: None, + inplace_multiply: None, + inplace_remainder: None, + inplace_divmod: None, + inplace_power: None, + inplace_lshift: None, + inplace_rshift: None, + inplace_and: None, + inplace_xor: None, + inplace_or: None, + floor_divide: None, + true_divide: None, + inplace_floor_divide: None, + inplace_true_divide: None, + index: None, + matrix_multiply: None, + inplace_matrix_multiply: None, + }; +} + +pub struct PyNumber<'a> { + pub obj: &'a PyObject, + // some fast path do not need methods, so we do lazy initialize + pub methods: Option<&'static PyNumberMethods>, +} + +impl<'a> PyNumber<'a> { + pub fn new(obj: &'a PyObject, vm: &VirtualMachine) -> Self { + Self { + obj, + methods: Self::find_methods(obj, vm), + } + } +} + +impl PyNumber<'_> { + pub fn find_methods(obj: &PyObject, vm: &VirtualMachine) -> Option<&'static PyNumberMethods> { + obj.class() + .mro_find_map(|x| x.slots.as_number.load()) + .map(|f| f(obj, vm)) + } + + pub fn methods(&self) -> &'static PyNumberMethods { + self.methods.unwrap_or(&PyNumberMethods::NOT_IMPLEMENTED) + } + + // PyNumber_Check + pub fn check(obj: &PyObject, vm: &VirtualMachine) -> bool { + Self::find_methods(obj, vm).map_or(false, |methods| { + methods.int.is_some() + || methods.index.is_some() + || methods.float.is_some() + || obj.payload_is::() + }) + } + + // PyIndex_Check + pub fn is_index(&self) -> bool { + self.methods().index.is_some() + } + + pub fn int(&self, vm: &VirtualMachine) -> PyResult { + fn try_convert(obj: &PyObject, lit: &[u8], vm: &VirtualMachine) -> PyResult { + let base = 10; + match int::bytes_to_int(lit, base) { + Some(i) => Ok(PyInt::from(i).into_ref(vm)), + None => Err(vm.new_value_error(format!( + "invalid literal for int() with base {}: {}", + base, + obj.repr(vm)?, + ))), + } + } + + if let Some(i) = self.obj.downcast_ref_if_exact::(vm) { + Ok(i.to_owned()) + } else if let Some(f) = self.methods().int { + let ret = f(self, vm)?; + if !ret.class().is(PyInt::class(vm)) { + warnings::warn( + vm.ctx.exceptions.deprecation_warning, + format!( + "__int__ returned non-int (type {}). \ + The ability to return an instance of a strict subclass of int \ + is deprecated, and may be removed in a future version of Python.", + ret.class() + ), + 1, + vm, + )?; + Ok(vm.ctx.new_bigint(ret.as_bigint())) + } else { + Ok(ret) + } + } else if self.methods().index.is_some() { + self.index(vm) + } else if let Ok(Ok(f)) = + vm.get_special_method(self.obj.to_owned(), identifier!(vm, __trunc__)) + { + // TODO: Deprecate in 3.11 + // warnings::warn( + // vm.ctx.exceptions.deprecation_warning.clone(), + // "The delegation of int() to __trunc__ is deprecated.".to_owned(), + // 1, + // vm, + // )?; + let ret = f.invoke((), vm)?; + PyNumber::new(ret.as_ref(), vm).index(vm).map_err(|_| { + vm.new_type_error(format!( + "__trunc__ returned non-Integral (type {})", + ret.class() + )) + }) + } else if let Some(s) = self.obj.payload::() { + try_convert(self.obj, s.as_str().as_bytes(), vm) + } else if let Some(bytes) = self.obj.payload::() { + try_convert(self.obj, bytes, vm) + } else if let Some(bytearray) = self.obj.payload::() { + try_convert(self.obj, &bytearray.borrow_buf(), vm) + } else if let Ok(buffer) = ArgBytesLike::try_from_borrowed_object(vm, self.obj) { + // TODO: replace to PyBuffer + try_convert(self.obj, &buffer.borrow_buf(), vm) + } else { + Err(vm.new_type_error(format!( + "int() argument must be a string, a bytes-like object or a real number, not '{}'", + self.obj.class() + ))) + } + } + + pub fn index_opt(&self, vm: &VirtualMachine) -> PyResult> { + if let Some(i) = self.obj.downcast_ref_if_exact::(vm) { + Ok(Some(i.to_owned())) + } else if let Some(i) = self.obj.payload::() { + Ok(Some(vm.ctx.new_bigint(i.as_bigint()))) + } else if let Some(f) = self.methods().index { + let ret = f(self, vm)?; + if !ret.class().is(PyInt::class(vm)) { + warnings::warn( + vm.ctx.exceptions.deprecation_warning, + format!( + "__index__ returned non-int (type {}). \ + The ability to return an instance of a strict subclass of int \ + is deprecated, and may be removed in a future version of Python.", + ret.class() + ), + 1, + vm, + )?; + Ok(Some(vm.ctx.new_bigint(ret.as_bigint()))) + } else { + Ok(Some(ret)) + } + } else { + Ok(None) + } + } + + pub fn index(&self, vm: &VirtualMachine) -> PyResult { + self.index_opt(vm)?.ok_or_else(|| { + vm.new_type_error(format!( + "'{}' object cannot be interpreted as an integer", + self.obj.class() + )) + }) + } + + pub fn float_opt(&self, vm: &VirtualMachine) -> PyResult>> { + if let Some(float) = self.obj.downcast_ref_if_exact::(vm) { + Ok(Some(float.to_owned())) + } else if let Some(f) = self.methods().float { + let ret = f(self, vm)?; + if !ret.class().is(PyFloat::class(vm)) { + warnings::warn( + vm.ctx.exceptions.deprecation_warning, + format!( + "__float__ returned non-float (type {}). \ + The ability to return an instance of a strict subclass of float \ + is deprecated, and may be removed in a future version of Python.", + ret.class() + ), + 1, + vm, + )?; + Ok(Some(vm.ctx.new_float(ret.to_f64()))) + } else { + Ok(Some(ret)) + } + } else if self.methods().index.is_some() { + let i = self.index(vm)?; + let value = int::try_to_float(i.as_bigint(), vm)?; + Ok(Some(vm.ctx.new_float(value))) + } else if let Some(value) = self.obj.downcast_ref::() { + Ok(Some(vm.ctx.new_float(value.to_f64()))) + } else { + Ok(None) + } + } + + pub fn float(&self, vm: &VirtualMachine) -> PyResult> { + self.float_opt(vm)?.ok_or_else(|| { + vm.new_type_error(format!("must be real number, not {}", self.obj.class())) + }) + } +} diff --git a/vm/src/stdlib/mod.rs b/vm/src/stdlib/mod.rs index 8e133a0764..c791dafc6e 100644 --- a/vm/src/stdlib/mod.rs +++ b/vm/src/stdlib/mod.rs @@ -21,7 +21,7 @@ mod sysconfigdata; #[cfg(feature = "threading")] mod thread; pub mod time; -mod warnings; +pub mod warnings; mod weakref; #[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))] diff --git a/vm/src/stdlib/warnings.rs b/vm/src/stdlib/warnings.rs index edc3b108e1..7eca878a46 100644 --- a/vm/src/stdlib/warnings.rs +++ b/vm/src/stdlib/warnings.rs @@ -1,5 +1,22 @@ pub(crate) use _warnings::make_module; +use crate::{builtins::PyType, Py, PyResult, VirtualMachine}; + +pub fn warn( + category: &Py, + message: String, + stack_level: usize, + vm: &VirtualMachine, +) -> PyResult<()> { + // TODO: use rust warnings module + if let Ok(module) = vm.import("warnings", None, 0) { + if let Ok(func) = module.get_attr("warn", vm) { + let _ = vm.invoke(&func, (message, category.to_owned(), stack_level)); + } + } + Ok(()) +} + #[pymodule] mod _warnings { use crate::{ diff --git a/vm/src/types/slot.rs b/vm/src/types/slot.rs index 245e7a81c8..ae3099a284 100644 --- a/vm/src/types/slot.rs +++ b/vm/src/types/slot.rs @@ -1,13 +1,14 @@ use crate::common::{hash::PyHash, lock::PyRwLock}; use crate::{ - builtins::{PyInt, PyStrInterned, PyStrRef, PyType, PyTypeRef}, + builtins::{PyFloat, PyInt, PyStrInterned, PyStrRef, PyType, PyTypeRef}, bytecode::ComparisonOperator, convert::{ToPyObject, ToPyResult}, function::Either, function::{FromArgs, FuncArgs, OptionalArg, PyComparisonValue}, identifier, protocol::{ - PyBuffer, PyIterReturn, PyMapping, PyMappingMethods, PySequence, PySequenceMethods, + PyBuffer, PyIterReturn, PyMapping, PyMappingMethods, PyNumber, PyNumberMethods, PySequence, + PySequenceMethods, }, vm::Context, AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, @@ -29,7 +30,7 @@ pub struct PyTypeSlots { // Methods to implement standard operations // Method suites for standard classes - // tp_as_number + pub as_number: AtomicCell>, pub as_sequence: AtomicCell>, pub as_mapping: AtomicCell>, @@ -138,6 +139,7 @@ impl Default for PyTypeFlags { pub(crate) type GenericMethod = fn(&PyObject, FuncArgs, &VirtualMachine) -> PyResult; pub(crate) type AsMappingFunc = fn(&PyObject, &VirtualMachine) -> &'static PyMappingMethods; +pub(crate) type AsNumberFunc = fn(&PyObject, &VirtualMachine) -> &'static PyNumberMethods; pub(crate) type HashFunc = fn(&PyObject, &VirtualMachine) -> PyResult; // CallFunc = GenericMethod pub(crate) type GetattroFunc = fn(&PyObject, PyStrRef, &VirtualMachine) -> PyResult; @@ -338,6 +340,67 @@ fn as_sequence_generic(zelf: &PyObject, vm: &VirtualMachine) -> &'static PySeque static_as_sequence_generic(has_length, has_ass_item) } +pub(crate) fn static_as_number_generic( + has_int: bool, + has_float: bool, + has_index: bool, +) -> &'static PyNumberMethods { + static METHODS: &[PyNumberMethods] = &[ + new_generic(false, false, false), + new_generic(true, false, false), + new_generic(false, true, false), + new_generic(true, true, false), + new_generic(false, false, true), + new_generic(true, false, true), + new_generic(false, true, true), + new_generic(true, true, true), + ]; + + fn int(num: &PyNumber, vm: &VirtualMachine) -> PyResult> { + let ret = vm.call_special_method(num.obj.to_owned(), identifier!(vm, __int__), ())?; + ret.downcast::().map_err(|obj| { + vm.new_type_error(format!("__int__ returned non-int (type {})", obj.class())) + }) + } + fn float(num: &PyNumber, vm: &VirtualMachine) -> PyResult> { + let ret = vm.call_special_method(num.obj.to_owned(), identifier!(vm, __float__), ())?; + ret.downcast::().map_err(|obj| { + vm.new_type_error(format!( + "__float__ returned non-float (type {})", + obj.class() + )) + }) + } + fn index(num: &PyNumber, vm: &VirtualMachine) -> PyResult> { + let ret = vm.call_special_method(num.obj.to_owned(), identifier!(vm, __index__), ())?; + ret.downcast::().map_err(|obj| { + vm.new_type_error(format!("__index__ returned non-int (type {})", obj.class())) + }) + } + + const fn new_generic(has_int: bool, has_float: bool, has_index: bool) -> PyNumberMethods { + PyNumberMethods { + int: if has_int { Some(int) } else { None }, + float: if has_float { Some(float) } else { None }, + index: if has_index { Some(index) } else { None }, + ..PyNumberMethods::NOT_IMPLEMENTED + } + } + + let key = bool_int(has_int) | (bool_int(has_float) << 1) | (bool_int(has_index) << 2); + + &METHODS[key] +} + +fn as_number_wrapper(zelf: &PyObject, vm: &VirtualMachine) -> &'static PyNumberMethods { + let (has_int, has_float, has_index) = ( + zelf.class().has_attr(identifier!(vm, __int__)), + zelf.class().has_attr(identifier!(vm, __float__)), + zelf.class().has_attr(identifier!(vm, __index__)), + ); + static_as_number_generic(has_int, has_float, has_index) +} + fn hash_wrapper(zelf: &PyObject, vm: &VirtualMachine) -> PyResult { let hash_obj = vm.call_special_method(zelf.to_owned(), identifier!(vm, __hash__), ())?; match hash_obj.payload_if_subclass::(vm) { @@ -489,6 +552,9 @@ impl PyType { "__del__" => { update_slot!(del, del_wrapper); } + "__int__" | "__index__" | "__float__" => { + update_slot!(as_number, as_number_wrapper); + } _ => {} } } @@ -991,6 +1057,21 @@ pub trait AsSequence: PyPayload { } } +#[pyimpl] +pub trait AsNumber: PyPayload { + const AS_NUMBER: PyNumberMethods; + + #[inline] + #[pyslot] + fn as_number(_zelf: &PyObject, _vm: &VirtualMachine) -> &'static PyNumberMethods { + &Self::AS_NUMBER + } + + fn number_downcast<'a>(number: &'a PyNumber) -> &'a Py { + unsafe { number.obj.downcast_unchecked_ref() } + } +} + #[pyimpl] pub trait Iterable: PyPayload { #[pyslot] diff --git a/vm/src/vm/vm_ops.rs b/vm/src/vm/vm_ops.rs index e0606aefe7..f8af60b7a0 100644 --- a/vm/src/vm/vm_ops.rs +++ b/vm/src/vm/vm_ops.rs @@ -3,36 +3,20 @@ use crate::{ builtins::{PyInt, PyIntRef, PyStrInterned}, function::PyArithmeticValue, object::{AsObject, PyObject, PyObjectRef, PyResult}, - protocol::PyIterReturn, + protocol::{PyIterReturn, PyNumber}, types::PyComparisonOp, }; /// Collection of operators impl VirtualMachine { pub fn to_index_opt(&self, obj: PyObjectRef) -> Option> { - match obj.downcast() { - Ok(val) => Some(Ok(val)), - Err(obj) => self - .get_method(obj, identifier!(self, __index__)) - .map(|index| { - // TODO: returning strict subclasses of int in __index__ is deprecated - self.invoke(&index?, ())?.downcast().map_err(|bad| { - self.new_type_error(format!( - "__index__ returned non-int (type {})", - bad.class().name() - )) - }) - }), - } + PyNumber::new(obj.as_ref(), self) + .index_opt(self) + .transpose() } pub fn to_index(&self, obj: &PyObject) -> PyResult { - self.to_index_opt(obj.to_owned()).unwrap_or_else(|| { - Err(self.new_type_error(format!( - "'{}' object cannot be interpreted as an integer", - obj.class().name() - ))) - }) + PyNumber::new(obj, self).index(self) } #[inline]