Skip to main content

rhai_bigint/
lib.rs

1#![doc = include_str!("../README.md")]
2
3//! Arbitrary-precision BigInt support for Rhai scripts.
4//!
5//! Provides [`BigIntPackage`] (via `def_package!`) for registering BigInt
6//! support into a Rhai [`Engine`](rhai::Engine).
7
8use num_bigint::BigInt;
9use rhai::{def_package, plugin::*};
10
11/// Builds a "Cannot compare {lhs} with {rhs}; {advice}" runtime error.
12#[cold]
13#[inline(never)]
14fn cross_type_cmp_err(
15    lhs: &'static str,
16    rhs: &'static str,
17    advice: &'static str,
18) -> Box<rhai::EvalAltResult> {
19    format!("Cannot compare {lhs} with {rhs}; {advice}").into()
20}
21
22/// Generates an `#[export_module]` that raises a runtime error whenever a `BigInt`
23/// is compared (via `==`, `!=`, `<`, `<=`, `>`, or `>=`) with `$t`, in either operand order.
24macro_rules! bigint_cross_type_cmp_module {
25    ($mod_name:ident, $t:ty, $type_name:literal, $advice:literal) => {
26        #[export_module]
27        pub(crate) mod $mod_name {
28            use num_bigint::BigInt;
29            use rhai::plugin::*;
30
31            #[rhai_fn(name = "==", pure, return_raw)]
32            pub fn eq_bigint(_l: &mut BigInt, _r: $t) -> Result<bool, Box<rhai::EvalAltResult>> {
33                Err(crate::cross_type_cmp_err("BigInt", $type_name, $advice))
34            }
35
36            #[rhai_fn(name = "!=", pure, return_raw)]
37            pub fn ne_bigint(_l: &mut BigInt, _r: $t) -> Result<bool, Box<rhai::EvalAltResult>> {
38                Err(crate::cross_type_cmp_err("BigInt", $type_name, $advice))
39            }
40
41            #[rhai_fn(name = "==", pure, return_raw)]
42            pub fn eq_type(_l: &mut $t, _r: BigInt) -> Result<bool, Box<rhai::EvalAltResult>> {
43                Err(crate::cross_type_cmp_err($type_name, "BigInt", $advice))
44            }
45
46            #[rhai_fn(name = "!=", pure, return_raw)]
47            pub fn ne_type(_l: &mut $t, _r: BigInt) -> Result<bool, Box<rhai::EvalAltResult>> {
48                Err(crate::cross_type_cmp_err($type_name, "BigInt", $advice))
49            }
50
51            #[rhai_fn(name = "<", pure, return_raw)]
52            pub fn lt_bigint(_l: &mut BigInt, _r: $t) -> Result<bool, Box<rhai::EvalAltResult>> {
53                Err(crate::cross_type_cmp_err("BigInt", $type_name, $advice))
54            }
55
56            #[rhai_fn(name = "<=", pure, return_raw)]
57            pub fn le_bigint(_l: &mut BigInt, _r: $t) -> Result<bool, Box<rhai::EvalAltResult>> {
58                Err(crate::cross_type_cmp_err("BigInt", $type_name, $advice))
59            }
60
61            #[rhai_fn(name = ">", pure, return_raw)]
62            pub fn gt_bigint(_l: &mut BigInt, _r: $t) -> Result<bool, Box<rhai::EvalAltResult>> {
63                Err(crate::cross_type_cmp_err("BigInt", $type_name, $advice))
64            }
65
66            #[rhai_fn(name = ">=", pure, return_raw)]
67            pub fn ge_bigint(_l: &mut BigInt, _r: $t) -> Result<bool, Box<rhai::EvalAltResult>> {
68                Err(crate::cross_type_cmp_err("BigInt", $type_name, $advice))
69            }
70
71            #[rhai_fn(name = "<", pure, return_raw)]
72            pub fn lt_type(_l: &mut $t, _r: BigInt) -> Result<bool, Box<rhai::EvalAltResult>> {
73                Err(crate::cross_type_cmp_err($type_name, "BigInt", $advice))
74            }
75
76            #[rhai_fn(name = "<=", pure, return_raw)]
77            pub fn le_type(_l: &mut $t, _r: BigInt) -> Result<bool, Box<rhai::EvalAltResult>> {
78                Err(crate::cross_type_cmp_err($type_name, "BigInt", $advice))
79            }
80
81            #[rhai_fn(name = ">", pure, return_raw)]
82            pub fn gt_type(_l: &mut $t, _r: BigInt) -> Result<bool, Box<rhai::EvalAltResult>> {
83                Err(crate::cross_type_cmp_err($type_name, "BigInt", $advice))
84            }
85
86            #[rhai_fn(name = ">=", pure, return_raw)]
87            pub fn ge_type(_l: &mut $t, _r: BigInt) -> Result<bool, Box<rhai::EvalAltResult>> {
88                Err(crate::cross_type_cmp_err($type_name, "BigInt", $advice))
89            }
90        }
91    };
92}
93
94#[export_module]
95mod bigint_functions {
96    use num_bigint::{BigInt, Sign};
97    use num_traits::{FromPrimitive, ToPrimitive, Zero};
98    use rhai::INT;
99
100    /// Creates a `BigInt` from an integer.
101    pub fn parse_bigint(value: INT) -> BigInt {
102        value.into()
103    }
104
105    /// Creates a `BigInt` from a float by truncating toward zero.
106    #[rhai_fn(name = "parse_bigint", return_raw)]
107    pub fn parse_bigint_from_float(value: rhai::FLOAT) -> Result<BigInt, Box<rhai::EvalAltResult>> {
108        BigInt::from_f64(value)
109            .ok_or_else(|| format!("Cannot convert {value} to BigInt: value must be finite").into())
110    }
111
112    /// Creates a `BigInt` from a string.
113    #[rhai_fn(name = "parse_bigint", return_raw)]
114    pub fn parse_bigint_from_str(value: String) -> Result<BigInt, Box<rhai::EvalAltResult>> {
115        value
116            .parse::<BigInt>()
117            .map_err(|e| format!("Cannot parse {value:?} as BigInt: {e}").into())
118    }
119
120    /// Converts an integer to a `BigInt` via method-call syntax: `42.to_bigint()`.
121    #[rhai_fn(name = "to_bigint", pure)]
122    pub fn int_to_bigint(value: &mut INT) -> BigInt {
123        BigInt::from(*value)
124    }
125
126    /// Converts a float to a `BigInt` by truncating toward zero.
127    /// Returns an error for non-finite values (infinity, NaN).
128    #[rhai_fn(name = "to_bigint", return_raw, pure)]
129    pub fn float_to_bigint(value: &mut rhai::FLOAT) -> Result<BigInt, Box<rhai::EvalAltResult>> {
130        BigInt::from_f64(*value)
131            .ok_or_else(|| format!("Cannot convert {value} to BigInt: value must be finite").into())
132    }
133
134    #[rhai_fn(name = "+", pure)]
135    pub fn add(l: &mut BigInt, r: BigInt) -> BigInt {
136        l.clone() + r
137    }
138
139    #[rhai_fn(name = "-", pure)]
140    pub fn sub(l: &mut BigInt, r: BigInt) -> BigInt {
141        l.clone() - r
142    }
143
144    #[rhai_fn(name = "*", pure)]
145    pub fn mul(l: &mut BigInt, r: BigInt) -> BigInt {
146        l.clone() * r
147    }
148
149    #[rhai_fn(name = "/", pure, return_raw)]
150    pub fn div(l: &mut BigInt, r: BigInt) -> Result<BigInt, Box<rhai::EvalAltResult>> {
151        if r.is_zero() {
152            return Err("Division by zero".into());
153        }
154        Ok(l.clone() / r)
155    }
156
157    #[rhai_fn(name = "%", pure, return_raw)]
158    pub fn rem(l: &mut BigInt, r: BigInt) -> Result<BigInt, Box<rhai::EvalAltResult>> {
159        if r.is_zero() {
160            return Err("Modulo by zero".into());
161        }
162        Ok(l.clone() % r)
163    }
164
165    #[rhai_fn(name = "-", pure)]
166    pub fn neg(value: &mut BigInt) -> BigInt {
167        -value.clone()
168    }
169
170    /// Raises a `BigInt` to an integer power. The exponent must be non-negative
171    /// and fit in a `u32`; returns an error otherwise.
172    #[rhai_fn(name = "**", pure, return_raw)]
173    pub fn pow(base: &mut BigInt, exp: INT) -> Result<BigInt, Box<rhai::EvalAltResult>> {
174        // The `exp < 0` branch is intentionally kept separate from the
175        // `u32::try_from` below. Both would reject negative values, but
176        // `try_from` would produce a generic "too large" message. The explicit
177        // branch gives users a clearer "must be non-negative" diagnostic.
178        if exp < 0 {
179            return Err(format!("Exponent must be non-negative, got {exp}").into());
180        }
181        let exp_u32 = u32::try_from(exp).map_err(|_| -> Box<rhai::EvalAltResult> {
182            format!("Exponent {exp} is too large, must be at most {}", u32::MAX).into()
183        })?;
184        Ok(base.clone().pow(exp_u32))
185    }
186
187    #[rhai_fn(name = "&", pure)]
188    pub fn bitand(l: &mut BigInt, r: BigInt) -> BigInt {
189        l.clone() & r
190    }
191
192    #[rhai_fn(name = "|", pure)]
193    pub fn bitor(l: &mut BigInt, r: BigInt) -> BigInt {
194        l.clone() | r
195    }
196
197    #[rhai_fn(name = "^", pure)]
198    pub fn bitxor(l: &mut BigInt, r: BigInt) -> BigInt {
199        l.clone() ^ r
200    }
201
202    fn validate_shift_amount(shift: INT) -> Result<u32, Box<rhai::EvalAltResult>> {
203        // Explicit negative check for the same reason as in `pow`: `try_from`
204        // would also reject negatives, but with a "too large" message instead
205        // of the more accurate "must be non-negative" one.
206        if shift < 0 {
207            return Err(format!("Shift amount must be non-negative, got {shift}").into());
208        }
209
210        u32::try_from(shift).map_err(|_| -> Box<rhai::EvalAltResult> {
211            format!("Shift amount is too large, must be at most {}", u32::MAX).into()
212        })
213    }
214
215    /// Left-shifts a `BigInt` by `shift` bits. `shift` must be non-negative and fit in `u32`.
216    #[rhai_fn(name = "<<", pure, return_raw)]
217    pub fn shl(value: &mut BigInt, shift: INT) -> Result<BigInt, Box<rhai::EvalAltResult>> {
218        let shift_u32 = validate_shift_amount(shift)?;
219        Ok(value.clone() << shift_u32)
220    }
221
222    /// Right-shifts a `BigInt` by `shift` bits. `shift` must be non-negative and fit in `u32`.
223    #[rhai_fn(name = ">>", pure, return_raw)]
224    pub fn shr(value: &mut BigInt, shift: INT) -> Result<BigInt, Box<rhai::EvalAltResult>> {
225        let shift_u32 = validate_shift_amount(shift)?;
226        Ok(value.clone() >> shift_u32)
227    }
228
229    #[rhai_fn(name = "==", pure)]
230    pub fn eq(l: &mut BigInt, r: BigInt) -> bool {
231        *l == r
232    }
233
234    #[rhai_fn(name = "!=", pure)]
235    pub fn ne(l: &mut BigInt, r: BigInt) -> bool {
236        *l != r
237    }
238
239    #[rhai_fn(name = "<", pure)]
240    pub fn lt(l: &mut BigInt, r: BigInt) -> bool {
241        *l < r
242    }
243
244    #[rhai_fn(name = "<=", pure)]
245    pub fn le(l: &mut BigInt, r: BigInt) -> bool {
246        *l <= r
247    }
248
249    #[rhai_fn(name = ">", pure)]
250    pub fn gt(l: &mut BigInt, r: BigInt) -> bool {
251        *l > r
252    }
253
254    #[rhai_fn(name = ">=", pure)]
255    pub fn ge(l: &mut BigInt, r: BigInt) -> bool {
256        *l >= r
257    }
258
259    /// Converts a `BigInt` to its decimal string representation.
260    #[rhai_fn(name = "to_string", pure)]
261    pub fn to_string(value: &mut BigInt) -> String {
262        value.to_string()
263    }
264
265    /// Converts a `BigInt` to a `0x`-prefixed lowercase hex string.
266    /// Negative values are prefixed with `-0x`.
267    #[rhai_fn(name = "to_hex", pure)]
268    pub fn to_hex(value: &mut BigInt) -> String {
269        let hex = format!("{:x}", value.magnitude());
270        if value.sign() == Sign::Minus {
271            format!("-0x{hex}")
272        } else {
273            format!("0x{hex}")
274        }
275    }
276
277    /// Converts a `BigInt` to a float, returning an error if the value
278    /// is too large to represent as a finite float.
279    #[rhai_fn(name = "to_float", pure, return_raw)]
280    pub fn to_float(value: &mut BigInt) -> Result<rhai::FLOAT, Box<rhai::EvalAltResult>> {
281        value
282            .to_f64()
283            .map(|f| f as rhai::FLOAT)
284            .filter(|f| f.is_finite())
285            .ok_or_else(|| {
286                "BigInt value is out of range for float (magnitude overflows to infinity)".into()
287            })
288    }
289}
290
291bigint_cross_type_cmp_module!(
292    bigint_int_cmp,
293    rhai::INT,
294    "int",
295    "wrap the int first: x == parse_bigint(42)"
296);
297
298bigint_cross_type_cmp_module!(
299    bigint_float_cmp,
300    rhai::FLOAT,
301    "float",
302    "convert the float to BigInt first via to_bigint() (truncates toward zero): x.to_bigint() == y"
303);
304
305bigint_cross_type_cmp_module!(
306    bigint_string_cmp,
307    rhai::ImmutableString,
308    "string",
309    r#"parse both sides first: parse_bigint(x) == parse_bigint(y)"#
310);
311
312bigint_cross_type_cmp_module!(
313    bigint_bool_cmp,
314    bool,
315    "bool",
316    "convert the BigInt to int first if a boolean check is needed: x != parse_bigint(0)"
317);
318
319def_package! {
320    /// Arbitrary-precision BigInt for Rhai: `bigint()` constructor plus
321    /// arithmetic (`+`, `-`, `*`, `/`, `%`), unary negation (`-`), and comparison operators.
322    pub BigIntPackage(lib) {
323        lib.set_custom_type::<BigInt>("BigInt");
324        combine_with_exported_module!(lib, "bigint", bigint_functions);
325        combine_with_exported_module!(lib, "bigint_int_cmp", bigint_int_cmp);
326        combine_with_exported_module!(lib, "bigint_float_cmp", bigint_float_cmp);
327        combine_with_exported_module!(lib, "bigint_string_cmp", bigint_string_cmp);
328        combine_with_exported_module!(lib, "bigint_bool_cmp", bigint_bool_cmp);
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use rhai::{packages::Package, Engine};
335
336    use super::*;
337
338    /// Asserts that `script` produces a runtime error whose message contains
339    /// `expected_fragment`, giving a clear failure message if either the eval
340    /// succeeds or the error text doesn't match.
341    #[track_caller]
342    fn assert_cmp_error(engine: &Engine, script: &str, expected_fragment: &str) {
343        match engine.eval::<bool>(script) {
344            Ok(v) => panic!("expected error for `{script}`, got Ok({v})"),
345            Err(e) => assert!(
346                e.to_string().contains(expected_fragment),
347                "script `{script}`\n  expected fragment: {expected_fragment:?}\n  actual error:    {e}"
348            ),
349        }
350    }
351
352    #[test]
353    fn test_rhai_integration() {
354        let mut engine = Engine::new();
355        BigIntPackage::new().register_into_engine(&mut engine);
356
357        let result: BigInt = engine.eval("parse_bigint(42)").unwrap();
358        assert_eq!(result.to_string(), "42");
359
360        let result: BigInt = engine
361            .eval("parse_bigint(\"123456789012345678901234567890\")")
362            .unwrap();
363        assert_eq!(result.to_string(), "123456789012345678901234567890");
364
365        let result: BigInt = engine.eval("parse_bigint(42) + parse_bigint(58)").unwrap();
366        assert_eq!(result.to_string(), "100");
367
368        let result: bool = engine.eval("parse_bigint(50) > parse_bigint(42)").unwrap();
369        assert!(result);
370
371        let result: bool = engine.eval("parse_bigint(42) == parse_bigint(42)").unwrap();
372        assert!(result);
373
374        let result: bool = engine
375            .eval("parse_bigint(42) != parse_bigint(100)")
376            .unwrap();
377        assert!(result);
378    }
379
380    // Integer literals like 1_000_000_000_000_000_000 exceed i32::MAX and are
381    // rejected by the Rhai parser under `only_i32`. Use `parse_bigint("…")` for
382    // large-value tests that must run in both configurations.
383    #[cfg(not(feature = "only_i32"))]
384    #[test]
385    fn test_core_arithmetic_i64_literals() {
386        let mut engine = Engine::new();
387        BigIntPackage::new().register_into_engine(&mut engine);
388
389        let result: BigInt = engine
390            .eval("parse_bigint(1000000000000000000) + parse_bigint(2000000000000000000)")
391            .unwrap();
392        assert_eq!(result.to_string(), "3000000000000000000");
393
394        let result: BigInt = engine
395            .eval("parse_bigint(5000000000000000000) - parse_bigint(1000000000000000000)")
396            .unwrap();
397        assert_eq!(result.to_string(), "4000000000000000000");
398
399        let result: BigInt = engine
400            .eval("parse_bigint(1000000) * parse_bigint(1000000)")
401            .unwrap();
402        assert_eq!(result.to_string(), "1000000000000");
403
404        let result: BigInt = engine
405            .eval("parse_bigint(1000000000000) / parse_bigint(1000000)")
406            .unwrap();
407        assert_eq!(result.to_string(), "1000000");
408
409        let result: BigInt = engine.eval("parse_bigint(10) % parse_bigint(3)").unwrap();
410        assert_eq!(result.to_string(), "1");
411
412        let result: BigInt = engine.eval("-parse_bigint(42)").unwrap();
413        assert_eq!(result.to_string(), "-42");
414    }
415
416    // Verify that INT-dispatch functions (`parse_bigint(INT)`, `.to_bigint()`,
417    // `**`, `<<`) work correctly at i32::MAX and i32::MIN boundaries.
418    #[cfg(feature = "only_i32")]
419    #[test]
420    fn test_only_i32_int_dispatch_boundaries() {
421        let mut engine = Engine::new();
422        BigIntPackage::new().register_into_engine(&mut engine);
423
424        // parse_bigint(INT) at i32::MAX / i32::MIN
425        let result: BigInt = engine.eval("parse_bigint(2147483647)").unwrap();
426        assert_eq!(result.to_string(), "2147483647");
427
428        let result: BigInt = engine.eval("parse_bigint(-2147483648)").unwrap();
429        assert_eq!(result.to_string(), "-2147483648");
430
431        // int.to_bigint() at i32::MAX
432        let result: BigInt = engine.eval("(2147483647).to_bigint()").unwrap();
433        assert_eq!(result.to_string(), "2147483647");
434
435        // pow with INT exponent: 2 ** 30 (largest safe i32 exponent)
436        let result: BigInt = engine.eval("parse_bigint(2) ** 30").unwrap();
437        assert_eq!(result.to_string(), "1073741824");
438
439        // shift with INT shift amount: 1 << 31
440        let result: BigInt = engine.eval("parse_bigint(1) << 31").unwrap();
441        assert_eq!(result.to_string(), "2147483648");
442    }
443
444    // Equivalent arithmetic coverage using string-based construction so that
445    // large values work even when rhai::INT = i32 (the `only_i32` feature).
446    #[cfg(feature = "only_i32")]
447    #[test]
448    fn test_core_arithmetic_string_literals() {
449        let mut engine = Engine::new();
450        BigIntPackage::new().register_into_engine(&mut engine);
451
452        let result: BigInt = engine
453            .eval(r#"parse_bigint("1000000000000000000") + parse_bigint("2000000000000000000")"#)
454            .unwrap();
455        assert_eq!(result.to_string(), "3000000000000000000");
456
457        let result: BigInt = engine
458            .eval(r#"parse_bigint("5000000000000000000") - parse_bigint("1000000000000000000")"#)
459            .unwrap();
460        assert_eq!(result.to_string(), "4000000000000000000");
461
462        let result: BigInt = engine
463            .eval(r#"parse_bigint("1000000") * parse_bigint("1000000")"#)
464            .unwrap();
465        assert_eq!(result.to_string(), "1000000000000");
466
467        let result: BigInt = engine
468            .eval(r#"parse_bigint("1000000000000") / parse_bigint("1000000")"#)
469            .unwrap();
470        assert_eq!(result.to_string(), "1000000");
471
472        let result: BigInt = engine
473            .eval(r#"parse_bigint("10") % parse_bigint("3")"#)
474            .unwrap();
475        assert_eq!(result.to_string(), "1");
476
477        let result: BigInt = engine.eval(r#"-parse_bigint("42")"#).unwrap();
478        assert_eq!(result.to_string(), "-42");
479    }
480
481    #[test]
482    fn test_error_handling() {
483        let mut engine = Engine::new();
484        BigIntPackage::new().register_into_engine(&mut engine);
485
486        let result = engine.eval::<BigInt>("parse_bigint(\"not_a_number\")");
487        assert!(result.is_err());
488
489        let result = engine.eval::<BigInt>("parse_bigint(42) / parse_bigint(0)");
490        assert!(result.is_err());
491
492        let result = engine.eval::<BigInt>("parse_bigint(42) % parse_bigint(0)");
493        assert!(result.is_err());
494    }
495
496    #[test]
497    fn test_parse_bigint_from_f64() {
498        let mut engine = Engine::new();
499        BigIntPackage::new().register_into_engine(&mut engine);
500
501        // fractional part is truncated toward zero
502        let result: BigInt = engine.eval("parse_bigint(1.5)").unwrap();
503        assert_eq!(result.to_string(), "1");
504
505        let result: BigInt = engine.eval("parse_bigint(-2.9)").unwrap();
506        assert_eq!(result.to_string(), "-2");
507
508        // exactly representable whole-number floats convert exactly
509        let result: BigInt = engine.eval("parse_bigint(42.0)").unwrap();
510        assert_eq!(result.to_string(), "42");
511
512        // large float that exceeds i64 range
513        let result: BigInt = engine.eval("parse_bigint(1e30)").unwrap();
514        assert_eq!(result.to_string(), "1000000000000000019884624838656");
515    }
516
517    #[test]
518    fn test_parse_bigint_from_f64_errors() {
519        let mut engine = Engine::new();
520        BigIntPackage::new().register_into_engine(&mut engine);
521
522        let result = engine.eval::<BigInt>("parse_bigint(1.0 / 0.0)");
523        assert!(result.is_err(), "infinity should be rejected");
524
525        let result = engine.eval::<BigInt>("parse_bigint(0.0 / 0.0)");
526        assert!(result.is_err(), "NaN should be rejected");
527    }
528
529    #[test]
530    fn test_to_string() {
531        let mut engine = Engine::new();
532        BigIntPackage::new().register_into_engine(&mut engine);
533
534        let result: String = engine.eval("parse_bigint(42).to_string()").unwrap();
535        assert_eq!(result, "42");
536
537        let result: String = engine.eval("parse_bigint(-99).to_string()").unwrap();
538        assert_eq!(result, "-99");
539
540        let result: String = engine
541            .eval("parse_bigint(\"123456789012345678901234567890\").to_string()")
542            .unwrap();
543        assert_eq!(result, "123456789012345678901234567890");
544    }
545
546    #[test]
547    fn test_to_hex() {
548        let mut engine = Engine::new();
549        BigIntPackage::new().register_into_engine(&mut engine);
550
551        let result: String = engine.eval("parse_bigint(255).to_hex()").unwrap();
552        assert_eq!(result, "0xff");
553
554        let result: String = engine.eval("parse_bigint(0).to_hex()").unwrap();
555        assert_eq!(result, "0x0");
556
557        let result: String = engine.eval("parse_bigint(-255).to_hex()").unwrap();
558        assert_eq!(result, "-0xff");
559
560        let result: String = engine.eval("parse_bigint(256).to_hex()").unwrap();
561        assert_eq!(result, "0x100");
562    }
563
564    #[test]
565    fn test_exponentiation() {
566        let mut engine = Engine::new();
567        BigIntPackage::new().register_into_engine(&mut engine);
568
569        let result: BigInt = engine.eval("parse_bigint(2) ** 10").unwrap();
570        assert_eq!(result.to_string(), "1024");
571
572        let result: BigInt = engine.eval("parse_bigint(10) ** 18").unwrap();
573        assert_eq!(result.to_string(), "1000000000000000000");
574
575        let result: BigInt = engine.eval("parse_bigint(-3) ** 3").unwrap();
576        assert_eq!(result.to_string(), "-27");
577
578        let result: BigInt = engine.eval("parse_bigint(5) ** 0").unwrap();
579        assert_eq!(result.to_string(), "1");
580
581        // negative exponent should be rejected
582        let result = engine.eval::<BigInt>("parse_bigint(2) ** -1");
583        assert!(result.is_err(), "negative exponent should be rejected");
584    }
585
586    #[test]
587    fn test_to_bigint_method() {
588        let mut engine = Engine::new();
589        BigIntPackage::new().register_into_engine(&mut engine);
590
591        // from integer
592        let result: BigInt = engine.eval("42.to_bigint()").unwrap();
593        assert_eq!(result.to_string(), "42");
594
595        let result: BigInt = engine.eval("(-99).to_bigint()").unwrap();
596        assert_eq!(result.to_string(), "-99");
597
598        let result: BigInt = engine.eval("0.to_bigint()").unwrap();
599        assert_eq!(result.to_string(), "0");
600
601        // from float — truncates toward zero
602        let result: BigInt = engine.eval("1.5.to_bigint()").unwrap();
603        assert_eq!(result.to_string(), "1");
604
605        let result: BigInt = engine.eval("(-2.9).to_bigint()").unwrap();
606        assert_eq!(result.to_string(), "-2");
607
608        let result: BigInt = engine.eval("1e30.to_bigint()").unwrap();
609        assert_eq!(result.to_string(), "1000000000000000019884624838656");
610
611        // non-finite floats should be rejected
612        let result = engine.eval::<BigInt>("(1.0 / 0.0).to_bigint()");
613        assert!(result.is_err(), "infinity should be rejected");
614
615        let result = engine.eval::<BigInt>("(0.0 / 0.0).to_bigint()");
616        assert!(result.is_err(), "NaN should be rejected");
617    }
618
619    #[test]
620    fn test_bitwise_operators() {
621        let mut engine = Engine::new();
622        BigIntPackage::new().register_into_engine(&mut engine);
623
624        // AND
625        let result: BigInt = engine
626            .eval("parse_bigint(0b1100) & parse_bigint(0b1010)")
627            .unwrap();
628        assert_eq!(result.to_string(), "8"); // 0b1000
629
630        let result: BigInt = engine.eval("parse_bigint(255) & parse_bigint(15)").unwrap();
631        assert_eq!(result.to_string(), "15");
632
633        // OR
634        let result: BigInt = engine
635            .eval("parse_bigint(0b1100) | parse_bigint(0b1010)")
636            .unwrap();
637        assert_eq!(result.to_string(), "14"); // 0b1110
638
639        let result: BigInt = engine.eval("parse_bigint(240) | parse_bigint(15)").unwrap();
640        assert_eq!(result.to_string(), "255");
641
642        // XOR
643        let result: BigInt = engine
644            .eval("parse_bigint(0b1100) ^ parse_bigint(0b1010)")
645            .unwrap();
646        assert_eq!(result.to_string(), "6"); // 0b0110
647
648        let result: BigInt = engine
649            .eval("parse_bigint(255) ^ parse_bigint(255)")
650            .unwrap();
651        assert_eq!(result.to_string(), "0");
652
653        // Left shift
654        let result: BigInt = engine.eval("parse_bigint(1) << 10").unwrap();
655        assert_eq!(result.to_string(), "1024");
656
657        let result: BigInt = engine.eval("parse_bigint(1) << 64").unwrap();
658        assert_eq!(result.to_string(), "18446744073709551616");
659
660        // Right shift
661        let result: BigInt = engine.eval("parse_bigint(1024) >> 3").unwrap();
662        assert_eq!(result.to_string(), "128");
663
664        let result: BigInt = engine.eval("parse_bigint(1) >> 1").unwrap();
665        assert_eq!(result.to_string(), "0");
666
667        // Negative shift amount should be rejected
668        let result = engine.eval::<BigInt>("parse_bigint(1) << -1");
669        assert!(result.is_err(), "negative left-shift should be rejected");
670
671        let result = engine.eval::<BigInt>("parse_bigint(1) >> -1");
672        assert!(result.is_err(), "negative right-shift should be rejected");
673    }
674
675    #[test]
676    fn test_cross_type_equality_errors() {
677        let mut engine = Engine::new();
678        BigIntPackage::new().register_into_engine(&mut engine);
679
680        // BigInt == int (both directions)
681        assert_cmp_error(
682            &engine,
683            "parse_bigint(42) == 42",
684            "Cannot compare BigInt with int",
685        );
686        assert_cmp_error(
687            &engine,
688            "parse_bigint(42) != 42",
689            "Cannot compare BigInt with int",
690        );
691        assert_cmp_error(
692            &engine,
693            "42 == parse_bigint(42)",
694            "Cannot compare int with BigInt",
695        );
696        assert_cmp_error(
697            &engine,
698            "42 != parse_bigint(42)",
699            "Cannot compare int with BigInt",
700        );
701
702        // BigInt == float (both directions)
703        assert_cmp_error(
704            &engine,
705            "parse_bigint(42) == 42.0",
706            "Cannot compare BigInt with float",
707        );
708        assert_cmp_error(
709            &engine,
710            "parse_bigint(42) != 42.0",
711            "Cannot compare BigInt with float",
712        );
713        assert_cmp_error(
714            &engine,
715            "42.0 == parse_bigint(42)",
716            "Cannot compare float with BigInt",
717        );
718        assert_cmp_error(
719            &engine,
720            "42.0 != parse_bigint(42)",
721            "Cannot compare float with BigInt",
722        );
723
724        // BigInt == string (both directions)
725        assert_cmp_error(
726            &engine,
727            r#"parse_bigint(42) == "42""#,
728            "Cannot compare BigInt with string",
729        );
730        assert_cmp_error(
731            &engine,
732            r#"parse_bigint(42) != "42""#,
733            "Cannot compare BigInt with string",
734        );
735        assert_cmp_error(
736            &engine,
737            r#""42" == parse_bigint(42)"#,
738            "Cannot compare string with BigInt",
739        );
740        assert_cmp_error(
741            &engine,
742            r#""42" != parse_bigint(42)"#,
743            "Cannot compare string with BigInt",
744        );
745
746        // BigInt == bool (both directions)
747        assert_cmp_error(
748            &engine,
749            "parse_bigint(1) == true",
750            "Cannot compare BigInt with bool",
751        );
752        assert_cmp_error(
753            &engine,
754            "parse_bigint(1) != true",
755            "Cannot compare BigInt with bool",
756        );
757        assert_cmp_error(
758            &engine,
759            "true == parse_bigint(1)",
760            "Cannot compare bool with BigInt",
761        );
762        assert_cmp_error(
763            &engine,
764            "true != parse_bigint(1)",
765            "Cannot compare bool with BigInt",
766        );
767
768        // BigInt == BigInt still works correctly
769        assert!(engine
770            .eval::<bool>("parse_bigint(42) == parse_bigint(42)")
771            .unwrap());
772        assert!(!engine
773            .eval::<bool>("parse_bigint(42) != parse_bigint(42)")
774            .unwrap());
775    }
776
777    #[test]
778    fn test_cross_type_ordering_errors() {
779        let mut engine = Engine::new();
780        BigIntPackage::new().register_into_engine(&mut engine);
781
782        // int — all four operators, both directions
783        for op in ["<", "<=", ">", ">="] {
784            assert_cmp_error(
785                &engine,
786                &format!("parse_bigint(42) {op} 42"),
787                "Cannot compare BigInt with int",
788            );
789            assert_cmp_error(
790                &engine,
791                &format!("42 {op} parse_bigint(42)"),
792                "Cannot compare int with BigInt",
793            );
794        }
795
796        // float — all four operators, both directions
797        for op in ["<", "<=", ">", ">="] {
798            assert_cmp_error(
799                &engine,
800                &format!("parse_bigint(42) {op} 42.0"),
801                "Cannot compare BigInt with float",
802            );
803            assert_cmp_error(
804                &engine,
805                &format!("42.0 {op} parse_bigint(42)"),
806                "Cannot compare float with BigInt",
807            );
808        }
809
810        // string — all four operators, both directions
811        for op in ["<", "<=", ">", ">="] {
812            assert_cmp_error(
813                &engine,
814                &format!(r#"parse_bigint(42) {op} "42""#),
815                "Cannot compare BigInt with string",
816            );
817            assert_cmp_error(
818                &engine,
819                &format!(r#""42" {op} parse_bigint(42)"#),
820                "Cannot compare string with BigInt",
821            );
822        }
823
824        // bool — all four operators, both directions
825        for op in ["<", "<=", ">", ">="] {
826            assert_cmp_error(
827                &engine,
828                &format!("parse_bigint(1) {op} true"),
829                "Cannot compare BigInt with bool",
830            );
831            assert_cmp_error(
832                &engine,
833                &format!("true {op} parse_bigint(1)"),
834                "Cannot compare bool with BigInt",
835            );
836        }
837
838        // BigInt ordering among themselves still works
839        assert!(engine
840            .eval::<bool>("parse_bigint(1) < parse_bigint(2)")
841            .unwrap());
842        assert!(engine
843            .eval::<bool>("parse_bigint(2) > parse_bigint(1)")
844            .unwrap());
845        assert!(engine
846            .eval::<bool>("parse_bigint(1) <= parse_bigint(1)")
847            .unwrap());
848        assert!(engine
849            .eval::<bool>("parse_bigint(1) >= parse_bigint(1)")
850            .unwrap());
851    }
852
853    /// All string-producing expressions in Rhai yield `ImmutableString` at
854    /// runtime, so every variant hits the same cross-type guard.
855    #[test]
856    fn test_cross_type_string_variants() {
857        let mut engine = Engine::new();
858        BigIntPackage::new().register_into_engine(&mut engine);
859
860        // String literal (base case — already in the general test, repeated here
861        // for clarity as the baseline for the variants below)
862        assert_cmp_error(
863            &engine,
864            r#"parse_bigint(42) == "42""#,
865            "Cannot compare BigInt with string",
866        );
867
868        // String stored in a variable
869        assert_cmp_error(
870            &engine,
871            r#"let s = "42"; parse_bigint(42) == s"#,
872            "Cannot compare BigInt with string",
873        );
874        assert_cmp_error(
875            &engine,
876            r#"let s = "42"; s == parse_bigint(42)"#,
877            "Cannot compare string with BigInt",
878        );
879
880        // String returned from a built-in function call (to_string on a plain int)
881        assert_cmp_error(
882            &engine,
883            "parse_bigint(42) == 42.to_string()",
884            "Cannot compare BigInt with string",
885        );
886        assert_cmp_error(
887            &engine,
888            "42.to_string() == parse_bigint(42)",
889            "Cannot compare string with BigInt",
890        );
891
892        // String produced by to_string() on a BigInt itself
893        assert_cmp_error(
894            &engine,
895            "parse_bigint(42) == parse_bigint(42).to_string()",
896            "Cannot compare BigInt with string",
897        );
898
899        // Template string / interpolation
900        assert_cmp_error(
901            &engine,
902            "parse_bigint(42) == `${42}`",
903            "Cannot compare BigInt with string",
904        );
905        assert_cmp_error(
906            &engine,
907            "`${42}` == parse_bigint(42)",
908            "Cannot compare string with BigInt",
909        );
910    }
911
912    #[test]
913    fn test_to_float() {
914        let mut engine = Engine::new();
915        BigIntPackage::new().register_into_engine(&mut engine);
916
917        let result: rhai::FLOAT = engine.eval("parse_bigint(42).to_float()").unwrap();
918        assert_eq!(result, 42.0);
919
920        let result: rhai::FLOAT = engine.eval("parse_bigint(-7).to_float()").unwrap();
921        assert_eq!(result, -7.0);
922
923        // value too large to be finite in f64
924        let result = engine.eval::<rhai::FLOAT>(
925            "parse_bigint(\"999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999\").to_float()"
926        );
927        assert!(result.is_err(), "overflow to infinity should be rejected");
928    }
929}