diff --git a/src/fable-library-py/src/floats.rs b/src/fable-library-py/src/floats.rs index 705ca30dc8..fcfa97e26f 100644 --- a/src/fable-library-py/src/floats.rs +++ b/src/fable-library-py/src/floats.rs @@ -602,75 +602,155 @@ float_variant!(Float64, f64); // Free functions for mathematical operations #[pyfunction] -pub fn abs(x: &Float64) -> Float64 { - Float64(x.0.abs()) +pub fn abs(x: &Bound<'_, PyAny>) -> PyResult { + // Fast path: x is already our type + if let Ok(x_val) = x.extract::() { + return Ok(Float64(x_val.0.abs())); + } + // Slow path: extract as primitive + let x_val = x.extract::()?; + Ok(Float64(x_val.abs())) } #[pyfunction] -pub fn sqrt(x: &Float64) -> PyResult { - x.sqrt() +pub fn sqrt(x: &Bound<'_, PyAny>) -> PyResult { + // Fast path: x is already our type + if let Ok(x_val) = x.extract::() { + return x_val.sqrt(); + } + // Slow path: extract as primitive + let x_val = x.extract::()?; + Float64(x_val).sqrt() } #[pyfunction] -pub fn cos(x: &Float64) -> Float64 { - x.cos() +pub fn cos(x: &Bound<'_, PyAny>) -> PyResult { + // Fast path: x is already our type + if let Ok(x_val) = x.extract::() { + return Ok(x_val.cos()); + } + // Slow path: extract as primitive + let x_val = x.extract::()?; + Ok(Float64(x_val).cos()) } #[pyfunction] -pub fn sin(x: &Float64) -> Float64 { - x.sin() +pub fn sin(x: &Bound<'_, PyAny>) -> PyResult { + // Fast path: x is already our type + if let Ok(x_val) = x.extract::() { + return Ok(x_val.sin()); + } + // Slow path: extract as primitive + let x_val = x.extract::()?; + Ok(Float64(x_val).sin()) } #[pyfunction] -pub fn tan(x: &Float64) -> Float64 { - x.tan() +pub fn tan(x: &Bound<'_, PyAny>) -> PyResult { + // Fast path: x is already our type + if let Ok(x_val) = x.extract::() { + return Ok(x_val.tan()); + } + // Slow path: extract as primitive + let x_val = x.extract::()?; + Ok(Float64(x_val).tan()) } #[pyfunction] -pub fn cosh(x: &Float64) -> Float64 { - x.cosh() +pub fn cosh(x: &Bound<'_, PyAny>) -> PyResult { + // Fast path: x is already our type + if let Ok(x_val) = x.extract::() { + return Ok(x_val.cosh()); + } + // Slow path: extract as primitive + let x_val = x.extract::()?; + Ok(Float64(x_val).cosh()) } #[pyfunction] -pub fn sinh(x: &Float64) -> Float64 { - x.sinh() +pub fn sinh(x: &Bound<'_, PyAny>) -> PyResult { + // Fast path: x is already our type + if let Ok(x_val) = x.extract::() { + return Ok(x_val.sinh()); + } + // Slow path: extract as primitive + let x_val = x.extract::()?; + Ok(Float64(x_val).sinh()) } #[pyfunction] -pub fn tanh(x: &Float64) -> Float64 { - x.tanh() +pub fn tanh(x: &Bound<'_, PyAny>) -> PyResult { + // Fast path: x is already our type + if let Ok(x_val) = x.extract::() { + return Ok(x_val.tanh()); + } + // Slow path: extract as primitive + let x_val = x.extract::()?; + Ok(Float64(x_val).tanh()) } #[pyfunction] -pub fn acos(x: &Float64) -> PyResult { - x.acos() +pub fn acos(x: &Bound<'_, PyAny>) -> PyResult { + // Fast path: x is already our type + if let Ok(x_val) = x.extract::() { + return x_val.acos(); + } + // Slow path: extract as primitive + let x_val = x.extract::()?; + Float64(x_val).acos() } #[pyfunction] -pub fn asin(x: &Float64) -> PyResult { - x.asin() +pub fn asin(x: &Bound<'_, PyAny>) -> PyResult { + // Fast path: x is already our type + if let Ok(x_val) = x.extract::() { + return x_val.asin(); + } + // Slow path: extract as primitive + let x_val = x.extract::()?; + Float64(x_val).asin() } #[pyfunction] -pub fn atan(x: &Float64) -> Float64 { - x.atan() +pub fn atan(x: &Bound<'_, PyAny>) -> PyResult { + // Fast path: x is already our type + if let Ok(x_val) = x.extract::() { + return Ok(x_val.atan()); + } + // Slow path: extract as primitive + let x_val = x.extract::()?; + Ok(Float64(x_val).atan()) } #[pyfunction] pub fn atan2(py: Python<'_>, y: &Bound<'_, PyAny>, x: &Bound<'_, PyAny>) -> PyResult> { - let y_val = y - .extract::() - .map_err(|_| PyErr::new::("Expected Float64 for y"))?; - let x_val = x - .extract::() - .map_err(|_| PyErr::new::("Expected Float64 for x"))?; + // Fast path: y is already our type + let y_val = if let Ok(y_val) = y.extract::() { + y_val + } else { + // Slow path: extract as primitive + Float64(y.extract::()?) + }; + // Fast path: x is already our type + let x_val = if let Ok(x_val) = x.extract::() { + x_val + } else { + // Slow path: extract as primitive + Float64(x.extract::()?) + }; let result = y_val.atan2(*x_val)?; Ok(result.into_pyobject(py)?.into()) } #[pyfunction] -pub fn exp(x: &Float64) -> Float64 { - x.exp() +pub fn exp(x: &Bound<'_, PyAny>) -> PyResult { + // Fast path: x is already our type + if let Ok(x_val) = x.extract::() { + return Ok(x_val.exp()); + } + // Slow path: extract as primitive + let x_val = x.extract::()?; + Ok(Float64(x_val).exp()) } #[pyfunction] @@ -680,9 +760,13 @@ pub fn log( x: &Bound<'_, PyAny>, base: Option<&Bound<'_, PyAny>>, ) -> PyResult> { - let f64_val = x - .extract::() - .map_err(|_| PyErr::new::("Expected Float64"))?; + // Fast path: x is already our type + let f64_val = if let Ok(f64_val) = x.extract::() { + f64_val + } else { + // Slow path: extract as primitive + Float64(x.extract::()?) + }; let base = match base { Some(b) => Some(b.extract::()?), None => None, @@ -692,86 +776,136 @@ pub fn log( } #[pyfunction] -pub fn log10(x: &Float64) -> Float64 { - x.log10() +pub fn log10(x: &Bound<'_, PyAny>) -> PyResult { + // Fast path: x is already our type + if let Ok(x_val) = x.extract::() { + return Ok(x_val.log10()); + } + // Slow path: extract as primitive + let x_val = x.extract::()?; + Ok(Float64(x_val).log10()) } #[pyfunction] -pub fn log2(x: &Float64) -> Float64 { - x.log2() +pub fn log2(x: &Bound<'_, PyAny>) -> PyResult { + // Fast path: x is already our type + if let Ok(x_val) = x.extract::() { + return Ok(x_val.log2()); + } + // Slow path: extract as primitive + let x_val = x.extract::()?; + Ok(Float64(x_val).log2()) } #[pyfunction] pub fn degrees(py: Python<'_>, x: &Bound<'_, PyAny>) -> PyResult> { - let f64_val = x - .extract::() - .map_err(|_| PyErr::new::("Expected Float64"))?; + // Fast path: x is already our type + let f64_val = if let Ok(f64_val) = x.extract::() { + f64_val + } else { + // Slow path: extract as primitive + Float64(x.extract::()?) + }; let result = f64_val.degrees(); Ok(result.into_pyobject(py)?.into()) } #[pyfunction] pub fn radians(py: Python<'_>, x: &Bound<'_, PyAny>) -> PyResult> { - let f64_val = x - .extract::() - .map_err(|_| PyErr::new::("Expected Float64"))?; + // Fast path: x is already our type + let f64_val = if let Ok(f64_val) = x.extract::() { + f64_val + } else { + // Slow path: extract as primitive + Float64(x.extract::()?) + }; let result = f64_val.radians(); Ok(result.into_pyobject(py)?.into()) } #[pyfunction] pub fn is_nan(_py: Python<'_>, x: &Bound<'_, PyAny>) -> PyResult { - let f64_val = x - .extract::() - .map_err(|_| PyErr::new::("Expected Float64"))?; + // Fast path: x is already our type + let f64_val = if let Ok(f64_val) = x.extract::() { + f64_val + } else { + // Slow path: extract as primitive + Float64(x.extract::()?) + }; Ok(f64_val.is_nan()) } #[pyfunction] pub fn is_infinity(_py: Python<'_>, x: &Bound<'_, PyAny>) -> PyResult { - let f64_val = x - .extract::() - .map_err(|_| PyErr::new::("Expected Float64"))?; + // Fast path: x is already our type + let f64_val = if let Ok(f64_val) = x.extract::() { + f64_val + } else { + // Slow path: extract as primitive + Float64(x.extract::()?) + }; Ok(f64_val.is_infinity()) } #[pyfunction] pub fn is_positive_infinity(_py: Python<'_>, x: &Bound<'_, PyAny>) -> PyResult { - let f64_val = x - .extract::() - .map_err(|_| PyErr::new::("Expected Float64"))?; + // Fast path: x is already our type + let f64_val = if let Ok(f64_val) = x.extract::() { + f64_val + } else { + // Slow path: extract as primitive + Float64(x.extract::()?) + }; Ok(f64_val.is_positive_infinity()) } #[pyfunction] pub fn is_negative_infinity(_py: Python<'_>, x: &Bound<'_, PyAny>) -> PyResult { - let f64_val = x - .extract::() - .map_err(|_| PyErr::new::("Expected Float64"))?; + // Fast path: x is already our type + let f64_val = if let Ok(f64_val) = x.extract::() { + f64_val + } else { + // Slow path: extract as primitive + Float64(x.extract::()?) + }; Ok(f64_val.is_negative_infinity()) } #[pyfunction] pub fn floor(py: Python<'_>, x: &Bound<'_, PyAny>) -> PyResult> { - let f64_val = x - .extract::() - .map_err(|_| PyErr::new::("Expected Float64"))?; + // Fast path: x is already our type + let f64_val = if let Ok(f64_val) = x.extract::() { + f64_val + } else { + // Slow path: extract as primitive + Float64(x.extract::()?) + }; let result = f64_val.floor(); Ok(result.into_pyobject(py)?.into()) } #[pyfunction] pub fn ceil(py: Python<'_>, x: &Bound<'_, PyAny>) -> PyResult> { - let f64_val = x - .extract::() - .map_err(|_| PyErr::new::("Expected Float64"))?; + // Fast path: x is already our type + let f64_val = if let Ok(f64_val) = x.extract::() { + f64_val + } else { + // Slow path: extract as primitive + Float64(x.extract::()?) + }; let result = f64_val.ceil(); Ok(result.into_pyobject(py)?.into()) } #[pyfunction] -pub fn pow(x: &Float64, y: &Bound<'_, PyAny>) -> PyResult { - x.__pow__(y, None) +pub fn pow(x: &Bound<'_, PyAny>, y: &Bound<'_, PyAny>) -> PyResult { + // Fast path: x is already our type + if let Ok(x_val) = x.extract::() { + return x_val.__pow__(y, None); + } + // Slow path: extract as primitive + let x_val = x.extract::()?; + Float64(x_val).__pow__(y, None) } #[pyfunction] diff --git a/src/fable-library-py/tests/test_floats.py b/src/fable-library-py/tests/test_floats.py index 9af0eb990e..3af2fc6920 100644 --- a/src/fable-library-py/tests/test_floats.py +++ b/src/fable-library-py/tests/test_floats.py @@ -205,6 +205,43 @@ def test_float64_power(): assert f64_a**2.0 == 10.5**2.0 +def test_float64_math_functions_accept_native_float(): + """The double module's free functions must accept a native float, not + just a Float64 instance, since e.g. big_int.to_double returns a native + float rather than a Float64 instance.""" + from fable_library import double + + native_float: float = float(10.5) + + assert double.pow(native_float, float64(2.0)) == 10.5**2.0 + assert double.pow(float64(10.5), float64(2.0)) == 10.5**2.0 + + assert double.floor(native_float) == 10.0 + assert double.ceil(native_float) == 11.0 + assert double.degrees(native_float) == math.degrees(10.5) + assert double.radians(native_float) == math.radians(10.5) + assert double.is_nan(native_float) is False + assert double.is_infinity(native_float) is False + assert double.is_positive_inf(native_float) is False + assert double.is_negative_inf(native_float) is False + assert double.sqrt(native_float) == math.sqrt(10.5) + assert double.cos(native_float) == math.cos(10.5) + assert double.sin(native_float) == math.sin(10.5) + assert double.tan(native_float) == math.tan(10.5) + assert double.cosh(native_float) == math.cosh(10.5) + assert double.sinh(native_float) == math.sinh(10.5) + assert double.tanh(native_float) == math.tanh(10.5) + assert double.acos(0.5) == math.acos(0.5) + assert double.asin(0.5) == math.asin(0.5) + assert double.atan(native_float) == math.atan(10.5) + assert double.atan2(native_float, native_float) == math.atan2(10.5, 10.5) + assert double.exp(native_float) == math.exp(10.5) + assert double.log(native_float) == math.log(10.5) + assert double.log10(native_float) == math.log10(10.5) + assert double.log2(native_float) == math.log2(10.5) + assert double.abs(-native_float) == 10.5 + + def test_float64_unary_operations(): """Test Float64 unary operations.""" f64_a = float64(10.5)