use std::f64::consts;
use num_bigint::BigInt;
use smallvec::smallvec;
use crate::{
args::{ArgValues, FromArgs},
bytecode::VM,
defer_drop, defer_drop_mut,
exception_private::{ExcType, ExcTypeExt, RunError, RunResult, SimpleException},
heap::{Heap, HeapData, HeapId},
intern::StaticStrings,
modules::ModuleFunctions,
types::{LongInt, Module, allocate_tuple},
value::Value,
};
fn math_domain_error() -> RunError {
SimpleException::new_msg(ExcType::ValueError, "math domain error").into()
}
fn math_range_error() -> RunError {
SimpleException::new_msg(ExcType::OverflowError, "math range error").into()
}
fn check_range_error(result: f64, input: f64) -> RunResult<()> {
if result.is_infinite() && input.is_finite() {
Err(math_range_error())
} else {
Ok(())
}
}
fn require_unit_range(f: f64) -> RunResult<()> {
if !f.is_nan() && !(-1.0..=1.0).contains(&f) {
Err(SimpleException::new_msg(
ExcType::ValueError,
format!("expected a number in range from -1 up to 1, got {f:?}"),
)
.into())
} else {
Ok(())
}
}
#[expect(
clippy::float_cmp,
reason = "exact comparison detects integer poles of gamma function"
)]
fn check_gamma_pole(f: f64) -> RunResult<()> {
if f <= 0.0 && f == f.floor() && f.is_finite() {
Err(SimpleException::new_msg(
ExcType::ValueError,
format!("expected a noninteger or positive integer, got {f:?}"),
)
.into())
} else {
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::Display, serde::Serialize, serde::Deserialize)]
#[strum(serialize_all = "lowercase")]
pub(crate) enum MathFunctions {
Floor,
Ceil,
Trunc,
Sqrt,
Isqrt,
Cbrt,
Pow,
Exp,
Exp2,
Expm1,
Log,
Log1p,
Log2,
Log10,
Fabs,
Isnan,
Isinf,
Isfinite,
Copysign,
Isclose,
Nextafter,
Ulp,
Sin,
Cos,
Tan,
Asin,
Acos,
Atan,
Atan2,
Sinh,
Cosh,
Tanh,
Asinh,
Acosh,
Atanh,
Degrees,
Radians,
Factorial,
Gcd,
Lcm,
Comb,
Perm,
Fmod,
Remainder,
Modf,
Frexp,
Ldexp,
Gamma,
Lgamma,
Erf,
Erfc,
}
pub fn create_module(vm: &mut VM<'_>) -> HeapId {
let mut module = Module::new(StaticStrings::Math);
for (name, func) in MATH_FUNCTIONS {
module.set_attr(*name, Value::ModuleFunction(ModuleFunctions::Math(*func)), vm);
}
module.set_attr(StaticStrings::Pi, Value::Float(consts::PI), vm);
module.set_attr(StaticStrings::MathE, Value::Float(consts::E), vm);
module.set_attr(StaticStrings::Tau, Value::Float(consts::TAU), vm);
module.set_attr(StaticStrings::MathInf, Value::Float(f64::INFINITY), vm);
module.set_attr(StaticStrings::MathNan, Value::Float(f64::NAN), vm);
vm.heap.allocate(HeapData::Module(Box::new(module)))
}
const MATH_FUNCTIONS: &[(StaticStrings, MathFunctions)] = &[
(StaticStrings::Floor, MathFunctions::Floor),
(StaticStrings::Ceil, MathFunctions::Ceil),
(StaticStrings::Trunc, MathFunctions::Trunc),
(StaticStrings::Sqrt, MathFunctions::Sqrt),
(StaticStrings::Isqrt, MathFunctions::Isqrt),
(StaticStrings::Cbrt, MathFunctions::Cbrt),
(StaticStrings::Pow, MathFunctions::Pow),
(StaticStrings::Exp, MathFunctions::Exp),
(StaticStrings::Exp2, MathFunctions::Exp2),
(StaticStrings::Expm1, MathFunctions::Expm1),
(StaticStrings::Log, MathFunctions::Log),
(StaticStrings::Log1p, MathFunctions::Log1p),
(StaticStrings::Log2, MathFunctions::Log2),
(StaticStrings::Log10, MathFunctions::Log10),
(StaticStrings::Fabs, MathFunctions::Fabs),
(StaticStrings::Isnan, MathFunctions::Isnan),
(StaticStrings::Isinf, MathFunctions::Isinf),
(StaticStrings::Isfinite, MathFunctions::Isfinite),
(StaticStrings::Copysign, MathFunctions::Copysign),
(StaticStrings::Isclose, MathFunctions::Isclose),
(StaticStrings::Nextafter, MathFunctions::Nextafter),
(StaticStrings::Ulp, MathFunctions::Ulp),
(StaticStrings::Sin, MathFunctions::Sin),
(StaticStrings::Cos, MathFunctions::Cos),
(StaticStrings::Tan, MathFunctions::Tan),
(StaticStrings::Asin, MathFunctions::Asin),
(StaticStrings::Acos, MathFunctions::Acos),
(StaticStrings::Atan, MathFunctions::Atan),
(StaticStrings::Atan2, MathFunctions::Atan2),
(StaticStrings::Sinh, MathFunctions::Sinh),
(StaticStrings::Cosh, MathFunctions::Cosh),
(StaticStrings::Tanh, MathFunctions::Tanh),
(StaticStrings::Asinh, MathFunctions::Asinh),
(StaticStrings::Acosh, MathFunctions::Acosh),
(StaticStrings::Atanh, MathFunctions::Atanh),
(StaticStrings::Degrees, MathFunctions::Degrees),
(StaticStrings::Radians, MathFunctions::Radians),
(StaticStrings::Factorial, MathFunctions::Factorial),
(StaticStrings::Gcd, MathFunctions::Gcd),
(StaticStrings::Lcm, MathFunctions::Lcm),
(StaticStrings::Comb, MathFunctions::Comb),
(StaticStrings::Perm, MathFunctions::Perm),
(StaticStrings::Fmod, MathFunctions::Fmod),
(StaticStrings::Remainder, MathFunctions::Remainder),
(StaticStrings::Modf, MathFunctions::Modf),
(StaticStrings::Frexp, MathFunctions::Frexp),
(StaticStrings::Ldexp, MathFunctions::Ldexp),
(StaticStrings::Gamma, MathFunctions::Gamma),
(StaticStrings::Lgamma, MathFunctions::Lgamma),
(StaticStrings::Erf, MathFunctions::Erf),
(StaticStrings::Erfc, MathFunctions::Erfc),
];
pub(super) fn call(vm: &mut VM<'_>, function: MathFunctions, args: ArgValues) -> RunResult<Value> {
match function {
MathFunctions::Floor => math_floor(vm, args),
MathFunctions::Ceil => math_ceil(vm, args),
MathFunctions::Trunc => math_trunc(vm, args),
MathFunctions::Sqrt => math_sqrt(vm, args),
MathFunctions::Isqrt => math_isqrt(vm, args),
MathFunctions::Cbrt => math_cbrt(vm, args),
MathFunctions::Pow => math_pow(vm, args),
MathFunctions::Exp => math_exp(vm, args),
MathFunctions::Exp2 => math_exp2(vm, args),
MathFunctions::Expm1 => math_expm1(vm, args),
MathFunctions::Log => math_log(vm, args),
MathFunctions::Log1p => math_log1p(vm, args),
MathFunctions::Log2 => math_log2(vm, args),
MathFunctions::Log10 => math_log10(vm, args),
MathFunctions::Fabs => math_fabs(vm, args),
MathFunctions::Isnan => math_isnan(vm, args),
MathFunctions::Isinf => math_isinf(vm, args),
MathFunctions::Isfinite => math_isfinite(vm, args),
MathFunctions::Copysign => math_copysign(vm, args),
MathFunctions::Isclose => math_isclose(vm, args),
MathFunctions::Nextafter => math_nextafter(vm, args),
MathFunctions::Ulp => math_ulp(vm, args),
MathFunctions::Sin => math_sin(vm, args),
MathFunctions::Cos => math_cos(vm, args),
MathFunctions::Tan => math_tan(vm, args),
MathFunctions::Asin => math_asin(vm, args),
MathFunctions::Acos => math_acos(vm, args),
MathFunctions::Atan => math_atan(vm, args),
MathFunctions::Atan2 => math_atan2(vm, args),
MathFunctions::Sinh => math_sinh(vm, args),
MathFunctions::Cosh => math_cosh(vm, args),
MathFunctions::Tanh => math_tanh(vm, args),
MathFunctions::Asinh => math_asinh(vm, args),
MathFunctions::Acosh => math_acosh(vm, args),
MathFunctions::Atanh => math_atanh(vm, args),
MathFunctions::Degrees => math_degrees(vm, args),
MathFunctions::Radians => math_radians(vm, args),
MathFunctions::Factorial => math_factorial(vm, args),
MathFunctions::Gcd => math_gcd(vm, args),
MathFunctions::Lcm => math_lcm(vm, args),
MathFunctions::Comb => math_comb(vm, args),
MathFunctions::Perm => math_perm(vm, args),
MathFunctions::Fmod => math_fmod(vm, args),
MathFunctions::Remainder => math_remainder(vm, args),
MathFunctions::Modf => math_modf(vm, args),
MathFunctions::Frexp => math_frexp(vm, args),
MathFunctions::Ldexp => math_ldexp(vm, args),
MathFunctions::Gamma => math_gamma(vm, args),
MathFunctions::Lgamma => math_lgamma(vm, args),
MathFunctions::Erf => math_erf(vm, args),
MathFunctions::Erfc => math_erfc(vm, args),
}
}
fn math_floor(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.floor", vm.heap)?;
defer_drop!(value, vm);
match value {
Value::Float(f) => float_to_int_checked(f.floor(), *f, vm.heap),
Value::Int(n) => Ok(Value::Int(*n)),
Value::Bool(b) => Ok(Value::Int(i64::from(*b))),
_ => Err(ExcType::type_error(format!(
"must be real number, not {}",
value.py_type_name(vm)
))),
}
}
fn math_ceil(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.ceil", vm.heap)?;
defer_drop!(value, vm);
match value {
Value::Float(f) => float_to_int_checked(f.ceil(), *f, vm.heap),
Value::Int(n) => Ok(Value::Int(*n)),
Value::Bool(b) => Ok(Value::Int(i64::from(*b))),
_ => Err(ExcType::type_error(format!(
"must be real number, not {}",
value.py_type_name(vm)
))),
}
}
fn math_trunc(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.trunc", vm.heap)?;
defer_drop!(value, vm);
match value {
Value::Float(f) => float_to_int_checked(f.trunc(), *f, vm.heap),
Value::Int(n) => Ok(Value::Int(*n)),
Value::Bool(b) => Ok(Value::Int(i64::from(*b))),
_ => Err(ExcType::type_error(format!(
"type {} doesn't define __trunc__ method",
value.py_type_name(vm)
))),
}
}
fn math_sqrt(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.sqrt", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
if f < 0.0 {
Err(SimpleException::new_msg(ExcType::ValueError, format!("expected a nonnegative input, got {f:?}")).into())
} else {
Ok(Value::Float(f.sqrt()))
}
}
fn math_isqrt(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.isqrt", vm.heap)?;
defer_drop!(value, vm);
let n = value_to_int(value, vm)?;
if n < 0 {
return Err(SimpleException::new_msg(ExcType::ValueError, "isqrt() argument must be nonnegative").into());
}
if n == 0 {
return Ok(Value::Int(0));
}
#[expect(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
reason = "initial estimate doesn't need to be exact, correction refines it"
)]
let mut x = (n as f64).sqrt() as i64;
while x > n / x {
x -= 1;
}
while x < n / (x + 1) {
x += 1;
}
Ok(Value::Int(x))
}
fn math_cbrt(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.cbrt", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
Ok(Value::Float(f.cbrt()))
}
fn math_pow(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let (x_val, y_val) = args.get_two_args("math.pow", vm.heap)?;
defer_drop!(x_val, vm);
defer_drop!(y_val, vm);
let x = value_to_float(x_val, vm)?;
let y = value_to_float(y_val, vm)?;
let result = x.powf(y);
if result.is_nan() && !x.is_nan() && !y.is_nan() {
return Err(math_domain_error());
}
if result.is_infinite() && x.is_finite() && y.is_finite() {
if x == 0.0 && y < 0.0 {
return Err(math_domain_error());
}
return Err(math_range_error());
}
Ok(Value::Float(result))
}
fn math_exp(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.exp", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
let result = f.exp();
check_range_error(result, f)?;
Ok(Value::Float(result))
}
fn math_exp2(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.exp2", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
let result = f.exp2();
check_range_error(result, f)?;
Ok(Value::Float(result))
}
fn math_expm1(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.expm1", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
let result = f.exp_m1();
check_range_error(result, f)?;
Ok(Value::Float(result))
}
fn math_log(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let (x_val, base_val) = args.get_one_two_args("math.log", vm.heap)?;
defer_drop!(x_val, vm);
defer_drop!(base_val, vm);
let x = value_to_float(x_val, vm)?;
if x <= 0.0 {
return Err(SimpleException::new_msg(ExcType::ValueError, "expected a positive input").into());
}
match base_val {
Some(base_v) => {
let base = value_to_float(base_v, vm)?;
#[expect(
clippy::float_cmp,
reason = "exact comparison with 1.0 is intentional — log(1.0) is exactly 0.0"
)]
if base == 1.0 {
return Err(SimpleException::new_msg(ExcType::ZeroDivisionError, "division by zero").into());
}
if base <= 0.0 {
return Err(SimpleException::new_msg(ExcType::ValueError, "expected a positive input").into());
}
Ok(Value::Float(x.ln() / base.ln()))
}
None => Ok(Value::Float(x.ln())),
}
}
fn math_log1p(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.log1p", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
if f <= -1.0 {
return Err(
SimpleException::new_msg(ExcType::ValueError, format!("expected argument value > -1, got {f:?}")).into(),
);
}
Ok(Value::Float(f.ln_1p()))
}
fn math_log2(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.log2", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
if f <= 0.0 {
Err(SimpleException::new_msg(ExcType::ValueError, "expected a positive input").into())
} else {
Ok(Value::Float(f.log2()))
}
}
fn math_log10(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.log10", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
if f <= 0.0 {
Err(SimpleException::new_msg(ExcType::ValueError, "expected a positive input").into())
} else {
Ok(Value::Float(f.log10()))
}
}
fn math_fabs(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.fabs", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
Ok(Value::Float(f.abs()))
}
fn math_isnan(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.isnan", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
Ok(Value::Bool(f.is_nan()))
}
fn math_isinf(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.isinf", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
Ok(Value::Bool(f.is_infinite()))
}
fn math_isfinite(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.isfinite", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
Ok(Value::Bool(f.is_finite()))
}
fn math_copysign(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let (x_val, y_val) = args.get_two_args("math.copysign", vm.heap)?;
defer_drop!(x_val, vm);
defer_drop!(y_val, vm);
let x = value_to_float(x_val, vm)?;
let y = value_to_float(y_val, vm)?;
Ok(Value::Float(x.copysign(y)))
}
fn math_isclose(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let IscloseArgs { a, b, rel_tol, abs_tol } = IscloseArgs::from_args(args, vm)?;
defer_drop!(a, vm);
defer_drop!(b, vm);
defer_drop!(rel_tol, vm);
defer_drop!(abs_tol, vm);
let a = value_to_float(a, vm)?;
let b = value_to_float(b, vm)?;
let rel_tol = value_to_float(rel_tol, vm)?;
let abs_tol = value_to_float(abs_tol, vm)?;
if rel_tol < 0.0 || abs_tol < 0.0 {
return Err(SimpleException::new_msg(ExcType::ValueError, "tolerances must be non-negative").into());
}
#[expect(
clippy::float_cmp,
reason = "exact equality check matches CPython's isclose() semantics"
)]
if a == b {
return Ok(Value::Bool(true));
}
if a.is_infinite() || b.is_infinite() {
return Ok(Value::Bool(false));
}
if a.is_nan() || b.is_nan() {
return Ok(Value::Bool(false));
}
let diff = (a - b).abs();
let result = diff <= (rel_tol * a.abs().max(b.abs())).max(abs_tol);
Ok(Value::Bool(result))
}
#[derive(FromArgs)]
#[from_args(name = "isclose")]
struct IscloseArgs {
a: Value,
b: Value,
#[from_args(kw_only, default = Value::Float(1e-9))]
rel_tol: Value,
#[from_args(kw_only, default = Value::Float(0.0))]
abs_tol: Value,
}
fn math_nextafter(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let (x_val, y_val) = args.get_two_args("math.nextafter", vm.heap)?;
defer_drop!(x_val, vm);
defer_drop!(y_val, vm);
let x = value_to_float(x_val, vm)?;
let y = value_to_float(y_val, vm)?;
Ok(Value::Float(libm::nextafter(x, y)))
}
fn math_ulp(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.ulp", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
if f.is_nan() {
return Ok(Value::Float(f64::NAN));
}
if f.is_infinite() {
return Ok(Value::Float(f64::INFINITY));
}
let f = f.abs();
if f == 0.0 {
return Ok(Value::Float(f64::from_bits(1)));
}
let next = libm::nextafter(f, f64::INFINITY);
Ok(Value::Float(next - f))
}
fn math_sin(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.sin", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
require_finite(f)?;
Ok(Value::Float(f.sin()))
}
fn math_cos(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.cos", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
require_finite(f)?;
Ok(Value::Float(f.cos()))
}
fn math_tan(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.tan", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
require_finite(f)?;
Ok(Value::Float(f.tan()))
}
fn math_asin(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.asin", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
require_unit_range(f)?;
Ok(Value::Float(f.asin()))
}
fn math_acos(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.acos", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
require_unit_range(f)?;
Ok(Value::Float(f.acos()))
}
fn math_atan(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.atan", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
Ok(Value::Float(f.atan()))
}
fn math_atan2(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let (y_val, x_val) = args.get_two_args("math.atan2", vm.heap)?;
defer_drop!(y_val, vm);
defer_drop!(x_val, vm);
let y = value_to_float(y_val, vm)?;
let x = value_to_float(x_val, vm)?;
Ok(Value::Float(y.atan2(x)))
}
fn math_sinh(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.sinh", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
let result = f.sinh();
check_range_error(result, f)?;
Ok(Value::Float(result))
}
fn math_cosh(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.cosh", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
let result = f.cosh();
check_range_error(result, f)?;
Ok(Value::Float(result))
}
fn math_tanh(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.tanh", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
Ok(Value::Float(f.tanh()))
}
fn math_asinh(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.asinh", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
Ok(Value::Float(f.asinh()))
}
fn math_acosh(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.acosh", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
if f < 1.0 {
return Err(SimpleException::new_msg(
ExcType::ValueError,
format!("expected argument value not less than 1, got {f:?}"),
)
.into());
}
Ok(Value::Float(f.acosh()))
}
fn math_atanh(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.atanh", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
if f <= -1.0 || f >= 1.0 {
return Err(SimpleException::new_msg(
ExcType::ValueError,
format!("expected a number between -1 and 1, got {f:?}"),
)
.into());
}
Ok(Value::Float(f.atanh()))
}
fn math_degrees(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.degrees", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
Ok(Value::Float(f.to_degrees()))
}
fn math_radians(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.radians", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
Ok(Value::Float(f.to_radians()))
}
fn math_factorial(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.factorial", vm.heap)?;
defer_drop!(value, vm);
let n = match value {
Value::Int(n) => *n,
Value::Bool(b) => i64::from(*b),
_ => {
return Err(ExcType::type_error(format!(
"'{}' object cannot be interpreted as an integer",
value.py_type_name(vm)
)));
}
};
if n < 0 {
return Err(
SimpleException::new_msg(ExcType::ValueError, "factorial() not defined for negative values").into(),
);
}
let mut result: i64 = 1;
for i in 2..=n {
match result.checked_mul(i) {
Some(v) => result = v,
None => {
return Err(
SimpleException::new_msg(ExcType::OverflowError, "int too large to convert to factorial").into(),
);
}
}
}
Ok(Value::Int(result))
}
fn math_gcd(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let positional = args.into_pos_only("math.gcd", vm.heap)?;
defer_drop_mut!(positional, vm);
let mut result: u64 = 0;
for arg in positional.by_ref() {
defer_drop!(arg, vm);
let n = value_to_int(arg, vm)?;
result = gcd(result, n.unsigned_abs());
}
Ok(u64_to_value(result, vm.heap))
}
fn math_lcm(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let positional = args.into_pos_only("math.lcm", vm.heap)?;
defer_drop_mut!(positional, vm);
let mut result: u64 = 1;
for arg in positional.by_ref() {
defer_drop!(arg, vm);
let n = value_to_int(arg, vm)?;
let abs_n = n.unsigned_abs();
if abs_n == 0 {
return Ok(Value::Int(0));
}
let g = gcd(result, abs_n);
result = (result / g)
.checked_mul(abs_n)
.ok_or_else(|| SimpleException::new_msg(ExcType::OverflowError, "integer overflow in lcm"))?;
}
Ok(u64_to_value(result, vm.heap))
}
fn math_comb(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let (n_val, k_val) = args.get_two_args("math.comb", vm.heap)?;
defer_drop!(n_val, vm);
defer_drop!(k_val, vm);
let n = value_to_int(n_val, vm)?;
let k = value_to_int(k_val, vm)?;
if n < 0 {
return Err(SimpleException::new_msg(ExcType::ValueError, "n must be a non-negative integer").into());
}
if k < 0 {
return Err(SimpleException::new_msg(ExcType::ValueError, "k must be a non-negative integer").into());
}
if k > n {
return Ok(Value::Int(0));
}
let k = k.min(n - k);
let mut result: i64 = 1;
for i in 0..k {
let mut numerator = n - i;
let mut denominator = i + 1;
#[expect(clippy::cast_sign_loss, reason = "both values are known non-negative at this point")]
let g = gcd(numerator as u64, denominator as u64).cast_signed();
numerator /= g;
denominator /= g;
#[expect(clippy::cast_sign_loss, reason = "result and denominator are known non-negative")]
let g2 = gcd(result as u64, denominator as u64).cast_signed();
result /= g2;
denominator /= g2;
debug_assert!(denominator == 1, "denominator should be 1 after GCD reduction in comb");
match result.checked_mul(numerator) {
Some(v) => result = v,
None => {
return Err(SimpleException::new_msg(ExcType::OverflowError, "integer overflow in comb").into());
}
}
}
Ok(Value::Int(result))
}
fn math_perm(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let (n_val, k_val) = args.get_one_two_args("math.perm", vm.heap)?;
defer_drop!(n_val, vm);
let n = value_to_int(n_val, vm)?;
let k_explicit = k_val.is_some();
let k = match k_val {
Some(kv) => {
defer_drop!(kv, vm);
value_to_int(kv, vm)?
}
None => n,
};
if n < 0 {
let msg = if k_explicit {
"n must be a non-negative integer"
} else {
"factorial() not defined for negative values"
};
return Err(SimpleException::new_msg(ExcType::ValueError, msg).into());
}
if k < 0 {
return Err(SimpleException::new_msg(ExcType::ValueError, "k must be a non-negative integer").into());
}
if k > n {
return Ok(Value::Int(0));
}
let mut result: i64 = 1;
for i in 0..k {
match result.checked_mul(n - i) {
Some(v) => result = v,
None => {
return Err(SimpleException::new_msg(ExcType::OverflowError, "integer overflow in perm").into());
}
}
}
Ok(Value::Int(result))
}
fn math_fmod(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let (x_val, y_val) = args.get_two_args("math.fmod", vm.heap)?;
defer_drop!(x_val, vm);
defer_drop!(y_val, vm);
let x = value_to_float(x_val, vm)?;
let y = value_to_float(y_val, vm)?;
if y == 0.0 || x.is_infinite() {
if !x.is_nan() && !y.is_nan() {
return Err(math_domain_error());
}
}
Ok(Value::Float(x % y))
}
fn math_remainder(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let (x_val, y_val) = args.get_two_args("math.remainder", vm.heap)?;
defer_drop!(x_val, vm);
defer_drop!(y_val, vm);
let x = value_to_float(x_val, vm)?;
let y = value_to_float(y_val, vm)?;
if x.is_nan() || y.is_nan() {
return Ok(Value::Float(f64::NAN));
}
if y == 0.0 {
return Err(math_domain_error());
}
if x.is_infinite() {
return Err(math_domain_error());
}
if y.is_infinite() {
return Ok(Value::Float(x));
}
Ok(Value::Float(libm::remainder(x, y)))
}
fn math_modf(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.modf", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
let (fractional, integer) = libm::modf(f);
let tuple = allocate_tuple(smallvec![Value::Float(fractional), Value::Float(integer)], vm.heap);
Ok(tuple)
}
fn math_frexp(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.frexp", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
let (m, exp) = libm::frexp(f);
let tuple = allocate_tuple(smallvec![Value::Float(m), Value::Int(i64::from(exp))], vm.heap);
Ok(tuple)
}
fn math_ldexp(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let (x_val, i_val) = args.get_two_args("math.ldexp", vm.heap)?;
defer_drop!(x_val, vm);
defer_drop!(i_val, vm);
let x = value_to_float(x_val, vm)?;
let i = value_to_int(i_val, vm)?;
if x.is_nan() || x.is_infinite() || x == 0.0 {
return Ok(Value::Float(x));
}
#[expect(clippy::cast_possible_truncation, reason = "clamped to i32 range first")]
let exp = i.clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32;
let result = libm::ldexp(x, exp);
if result.is_infinite() {
return Err(math_range_error());
}
Ok(Value::Float(result))
}
fn math_gamma(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.gamma", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
if f == f64::NEG_INFINITY {
return Err(SimpleException::new_msg(
ExcType::ValueError,
format!("expected a noninteger or positive integer, got {f:?}"),
)
.into());
}
check_gamma_pole(f)?;
let result = libm::tgamma(f);
check_range_error(result, f)?;
Ok(Value::Float(result))
}
fn math_lgamma(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.lgamma", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
check_gamma_pole(f)?;
let result = libm::lgamma(f);
check_range_error(result, f)?;
Ok(Value::Float(result))
}
fn math_erf(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.erf", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
Ok(Value::Float(libm::erf(f)))
}
fn math_erfc(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("math.erfc", vm.heap)?;
defer_drop!(value, vm);
let f = value_to_float(value, vm)?;
Ok(Value::Float(libm::erfc(f)))
}
fn float_to_int_checked(rounded: f64, original: f64, heap: &mut Heap) -> RunResult<Value> {
if original.is_infinite() {
Err(SimpleException::new_msg(ExcType::OverflowError, "cannot convert float infinity to integer").into())
} else if original.is_nan() {
Err(SimpleException::new_msg(ExcType::ValueError, "cannot convert float NaN to integer").into())
} else if rounded >= i64::MIN as f64 && rounded < i64::MAX as f64 {
#[expect(
clippy::cast_possible_truncation,
reason = "intentional: value is within i64 range after bounds check"
)]
let result = rounded as i64;
Ok(Value::Int(result))
} else {
let s = format!("{rounded:.0}");
let bi = s
.parse::<BigInt>()
.map_err(|_| SimpleException::new_msg(ExcType::ValueError, "float too large to convert to integer"))?;
Ok(LongInt::new(bi).into_value(heap))
}
}
#[expect(
clippy::cast_precision_loss,
reason = "i64-to-f64 can lose precision for large integers (beyond 2^53), but this matches CPython's conversion semantics"
)]
fn value_to_float(value: &Value, vm: &VM<'_>) -> RunResult<f64> {
match value {
Value::Float(f) => Ok(*f),
Value::Int(n) => Ok(*n as f64),
Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
_ => Err(ExcType::type_error(format!(
"must be real number, not {}",
value.py_type_name(vm)
))),
}
}
fn value_to_int(value: &Value, vm: &VM<'_>) -> RunResult<i64> {
match value {
Value::Int(n) => Ok(*n),
Value::Bool(b) => Ok(i64::from(*b)),
_ => Err(ExcType::type_error(format!(
"'{}' object cannot be interpreted as an integer",
value.py_type_name(vm)
))),
}
}
fn require_finite(f: f64) -> RunResult<()> {
if f.is_infinite() {
Err(SimpleException::new_msg(ExcType::ValueError, format!("expected a finite input, got {f:?}")).into())
} else {
Ok(())
}
}
fn gcd(mut a: u64, mut b: u64) -> u64 {
while b != 0 {
let t = b;
b = a % b;
a = t;
}
a
}
fn u64_to_value(n: u64, heap: &mut Heap) -> Value {
if let Ok(signed) = i64::try_from(n) {
Value::Int(signed)
} else {
LongInt::new(BigInt::from(n)).into_value(heap)
}
}