rhai-bigint 0.1.2

Rhai plugin providing arbitrary-precision BigInt support
Documentation
#![doc = include_str!("../README.md")]

//! Arbitrary-precision BigInt support for Rhai scripts.
//!
//! Provides [`BigIntPackage`] (via `def_package!`) for registering BigInt
//! support into a Rhai [`Engine`](rhai::Engine).

use num_bigint::BigInt;
use rhai::{def_package, plugin::*};

#[export_module]
mod bigint_functions {
    use num_bigint::BigInt;
    use num_traits::{FromPrimitive, Zero};

    /// Creates a `BigInt` from an integer.
    pub fn bigint(value: i64) -> BigInt {
        value.into()
    }

    /// Creates a `BigInt` from a float by truncating toward zero.
    #[rhai_fn(name = "bigint", return_raw)]
    pub fn bigint_from_float(value: rhai::FLOAT) -> Result<BigInt, Box<rhai::EvalAltResult>> {
        BigInt::from_f64(value)
            .ok_or_else(|| format!("Cannot convert {value} to BigInt: value must be finite").into())
    }

    /// Creates a `BigInt` from a string.
    #[rhai_fn(name = "bigint", return_raw)]
    pub fn bigint_from_str(value: String) -> Result<BigInt, Box<rhai::EvalAltResult>> {
        value
            .parse::<BigInt>()
            .map_err(|e| format!("Failed to create BigInt from string: {e}").into())
    }

    #[rhai_fn(name = "+", pure)]
    pub fn add(l: &mut BigInt, r: BigInt) -> BigInt {
        l.clone() + r
    }

    #[rhai_fn(name = "-", pure)]
    pub fn sub(l: &mut BigInt, r: BigInt) -> BigInt {
        l.clone() - r
    }

    #[rhai_fn(name = "*", pure)]
    pub fn mul(l: &mut BigInt, r: BigInt) -> BigInt {
        l.clone() * r
    }

    #[rhai_fn(name = "/", pure, return_raw)]
    pub fn div(l: &mut BigInt, r: BigInt) -> Result<BigInt, Box<rhai::EvalAltResult>> {
        if r.is_zero() {
            return Err("Division by zero".into());
        }
        Ok(l.clone() / r)
    }

    #[rhai_fn(name = "%", pure, return_raw)]
    pub fn rem(l: &mut BigInt, r: BigInt) -> Result<BigInt, Box<rhai::EvalAltResult>> {
        if r.is_zero() {
            return Err("Modulo by zero".into());
        }
        Ok(l.clone() % r)
    }

    #[rhai_fn(name = "-", pure)]
    pub fn neg(value: &mut BigInt) -> BigInt {
        -value.clone()
    }

    #[rhai_fn(name = "==", pure)]
    pub fn eq(l: &mut BigInt, r: BigInt) -> bool {
        *l == r
    }

    #[rhai_fn(name = "!=", pure)]
    pub fn ne(l: &mut BigInt, r: BigInt) -> bool {
        *l != r
    }

    #[rhai_fn(name = "<", pure)]
    pub fn lt(l: &mut BigInt, r: BigInt) -> bool {
        *l < r
    }

    #[rhai_fn(name = "<=", pure)]
    pub fn le(l: &mut BigInt, r: BigInt) -> bool {
        *l <= r
    }

    #[rhai_fn(name = ">", pure)]
    pub fn gt(l: &mut BigInt, r: BigInt) -> bool {
        *l > r
    }

    #[rhai_fn(name = ">=", pure)]
    pub fn ge(l: &mut BigInt, r: BigInt) -> bool {
        *l >= r
    }
}

def_package! {
    /// Arbitrary-precision BigInt for Rhai: `bigint()` constructor plus
    /// arithmetic (`+`, `-`, `*`, `/`, `%`), unary negation (`-`), and comparison operators.
    pub BigIntPackage(lib) {
        lib.set_custom_type::<BigInt>("BigInt");
        combine_with_exported_module!(lib, "bigint", bigint_functions);
    }
}

#[cfg(test)]
mod tests {
    use rhai::{Engine, packages::Package};

    use super::*;

    #[test]
    fn test_rhai_integration() {
        let mut engine = Engine::new();
        BigIntPackage::new().register_into_engine(&mut engine);

        let result: BigInt = engine.eval("bigint(42)").unwrap();
        assert_eq!(result.to_string(), "42");

        let result: BigInt = engine
            .eval("bigint(\"123456789012345678901234567890\")")
            .unwrap();
        assert_eq!(result.to_string(), "123456789012345678901234567890");

        let result: BigInt = engine.eval("bigint(42) + bigint(58)").unwrap();
        assert_eq!(result.to_string(), "100");

        let result: bool = engine.eval("bigint(50) > bigint(42)").unwrap();
        assert!(result);

        let result: bool = engine.eval("bigint(42) == bigint(42)").unwrap();
        assert!(result);

        let result: bool = engine.eval("bigint(42) != bigint(100)").unwrap();
        assert!(result);
    }

    #[test]
    fn test_core_functionality() {
        let mut engine = Engine::new();
        BigIntPackage::new().register_into_engine(&mut engine);

        let result: BigInt = engine
            .eval("bigint(1000000000000000000) + bigint(2000000000000000000)")
            .unwrap();
        assert_eq!(result.to_string(), "3000000000000000000");

        let result: BigInt = engine
            .eval("bigint(5000000000000000000) - bigint(1000000000000000000)")
            .unwrap();
        assert_eq!(result.to_string(), "4000000000000000000");

        let result: BigInt = engine.eval("bigint(1000000) * bigint(1000000)").unwrap();
        assert_eq!(result.to_string(), "1000000000000");

        let result: BigInt = engine
            .eval("bigint(1000000000000) / bigint(1000000)")
            .unwrap();
        assert_eq!(result.to_string(), "1000000");

        let result: BigInt = engine.eval("bigint(10) % bigint(3)").unwrap();
        assert_eq!(result.to_string(), "1");

        let result: BigInt = engine.eval("-bigint(42)").unwrap();
        assert_eq!(result.to_string(), "-42");
    }

    #[test]
    fn test_error_handling() {
        let mut engine = Engine::new();
        BigIntPackage::new().register_into_engine(&mut engine);

        let result = engine.eval::<BigInt>("bigint(\"not_a_number\")");
        assert!(result.is_err());

        let result = engine.eval::<BigInt>("bigint(42) / bigint(0)");
        assert!(result.is_err());

        let result = engine.eval::<BigInt>("bigint(42) % bigint(0)");
        assert!(result.is_err());
    }

    #[test]
    fn test_bigint_from_f64() {
        let mut engine = Engine::new();
        BigIntPackage::new().register_into_engine(&mut engine);

        // fractional part is truncated toward zero
        let result: BigInt = engine.eval("bigint(1.5)").unwrap();
        assert_eq!(result.to_string(), "1");

        let result: BigInt = engine.eval("bigint(-2.9)").unwrap();
        assert_eq!(result.to_string(), "-2");

        // exactly representable whole-number floats convert exactly
        let result: BigInt = engine.eval("bigint(42.0)").unwrap();
        assert_eq!(result.to_string(), "42");

        // large float that exceeds i64 range
        let result: BigInt = engine.eval("bigint(1e30)").unwrap();
        assert_eq!(result.to_string(), "1000000000000000019884624838656");
    }

    #[test]
    fn test_bigint_from_f64_errors() {
        let mut engine = Engine::new();
        BigIntPackage::new().register_into_engine(&mut engine);

        let result = engine.eval::<BigInt>("bigint(1.0 / 0.0)");
        assert!(result.is_err(), "infinity should be rejected");

        let result = engine.eval::<BigInt>("bigint(0.0 / 0.0)");
        assert!(result.is_err(), "NaN should be rejected");
    }
}