diff --git a/src/fable-library-py/src/floats.rs b/src/fable-library-py/src/floats.rs index fcfa97e26f..d76c5767d7 100644 --- a/src/fable-library-py/src/floats.rs +++ b/src/fable-library-py/src/floats.rs @@ -49,9 +49,9 @@ macro_rules! float_variant { // --- Arithmetic Methods --- // Fast path: check if other is our type first to avoid __float__ call pub fn __add__(&self, other: &Bound<'_, PyAny>) -> PyResult { - // Fast path: other is same type - if let Ok(other_val) = other.extract::() { - return Ok(Self(self.0 + other_val.0)); + // Fast path: other is same type (cast avoids a discarded PyErr on miss) + if let Ok(other_val) = other.cast::() { + return Ok(Self(self.0 + other_val.get().0)); } // Slow path: extract as primitive let other_val = other.extract::<$type>()?; @@ -63,8 +63,8 @@ macro_rules! float_variant { } pub fn __sub__(&self, other: &Bound<'_, PyAny>) -> PyResult { - if let Ok(other_val) = other.extract::() { - return Ok(Self(self.0 - other_val.0)); + if let Ok(other_val) = other.cast::() { + return Ok(Self(self.0 - other_val.get().0)); } let other_val = other.extract::<$type>()?; Ok(Self(self.0 - other_val)) @@ -79,8 +79,8 @@ macro_rules! float_variant { } pub fn __mul__(&self, other: &Bound<'_, PyAny>) -> PyResult { - if let Ok(other_val) = other.extract::() { - return Ok(Self(self.0 * other_val.0)); + if let Ok(other_val) = other.cast::() { + return Ok(Self(self.0 * other_val.get().0)); } let other_val = other.extract::<$type>()?; Ok(Self(self.0 * other_val)) @@ -196,6 +196,19 @@ macro_rules! float_variant { op: CompareOp, py: Python<'py>, ) -> PyResult> { + // Fast path: same type (cast avoids a discarded PyErr and a __float__ hop) + if let Ok(other_val) = other.cast::() { + let other_float = other_val.get().0; + let result = match op { + CompareOp::Eq => self.0 == other_float, + CompareOp::Ne => self.0 != other_float, + CompareOp::Lt => self.0 < other_float, + CompareOp::Le => self.0 <= other_float, + CompareOp::Gt => self.0 > other_float, + CompareOp::Ge => self.0 >= other_float, + }; + return Ok(PyBool::new(py, result).into_any()); + } // Try to convert other to our type first if let Ok(other_float) = other.extract::<$type>() { let result = match op { diff --git a/src/fable-library-py/src/ints.rs b/src/fable-library-py/src/ints.rs index 3ff41bb4aa..d408ebb593 100644 --- a/src/fable-library-py/src/ints.rs +++ b/src/fable-library-py/src/ints.rs @@ -476,6 +476,18 @@ macro_rules! integer_variant { /// Performs division with support for both integer and floating-point divisors. /// Returns zero division error for zero divisors. pub fn __truediv__(&self, other: &Bound<'_, PyAny>) -> PyResult<$name> { + // Fast path: same type — direct typed integer division. Avoids the + // __int__ hop of the OtherType extract, and the float-fallback + // precision loss for large/negative operands. + if let Ok(other_val) = other.cast::<$name>() { + let divisor = other_val.get().0; + if divisor == 0 { + return Err(PyErr::new::( + "Cannot divide by zero", + )); + } + return Ok($name(self.0 / divisor)); + } let value = other.extract::(); match value { Ok(OtherType::Int(value)) => { diff --git a/src/fable-library-py/tests/test_ints.py b/src/fable-library-py/tests/test_ints.py index fecf320c39..141154f329 100644 --- a/src/fable-library-py/tests/test_ints.py +++ b/src/fable-library-py/tests/test_ints.py @@ -1,6 +1,8 @@ from array import array from decimal import Decimal +import pytest + from fable_library.core import byte, int16, int32, int64, sbyte, uint16, uint32, uint64 from pydantic import BaseModel @@ -179,6 +181,24 @@ def test_divide(): assert byte(10) / 3.0 == 3.3333333333333335 +def test_divide_same_type(): + # Same-type division uses the typed integer fast path (truncates toward + # zero, matching .NET) rather than a float fallback. + assert int32(100) / int32(7) == 14 + assert int32(-100) / int32(7) == -14 + assert int32(100) / int32(-7) == -14 + assert int64(100) / int64(7) == 14 + assert byte(10) / byte(3) == 3 + # uint64 operand exceeding i64::MAX: the old OtherType path fell back to + # f64 division and lost precision; the typed path is exact. + assert uint64(18446744073709551615) / uint64(7) == 2635249153387078802 + + +def test_divide_same_type_by_zero(): + with pytest.raises(ZeroDivisionError): + int32(1) / int32(0) + + def test_hash(): assert hash(byte(42)) != 0 assert hash(sbyte(42)) != 0