Skip to main content

aver_rt/
int.rs

1//! Arbitrary-precision integer for the Aver runtime.
2//!
3//! Aver's `Int` is mathematical ℤ: total, never wrapping, faithful to the
4//! Lean/Dafny proof model. `AverInt` is the runtime carrier for that model.
5//! It is small-int optimized: any value that fits an `i64` is stored inline
6//! as `Small`, and only genuinely large magnitudes spill to a heap `BigInt`.
7//!
8//! Native machine-integer speed is a separate, opt-in concern (a bounded
9//! refinement type whose carrier the compiler lowers to raw `i64`); it is not
10//! this type's job. `AverInt` is correctness-first: every arithmetic operation
11//! produces the exact mathematical result.
12//!
13//! ## Canonical form
14//!
15//! The single invariant every constructor and operation upholds: a value that
16//! fits `i64` is **always** `Small`. A `Big` payload never holds a value in
17//! `[i64::MIN, i64::MAX]`. This makes the representation canonical, so derived
18//! `Eq`, `Ord`, and `Hash` are unique — two numerically-equal values always
19//! compare and hash identically regardless of how they were built. Map/Set
20//! keying depends on this.
21
22use core::cmp::Ordering;
23use core::fmt;
24use core::str::FromStr;
25
26use num_bigint::BigInt;
27use num_integer::Integer;
28use num_traits::{FromPrimitive, ToPrimitive, Zero};
29
30/// Arbitrary-precision integer (mathematical ℤ) with a small-int fast path.
31///
32/// Invariant: `Big` never holds a value representable as `i64` (see module
33/// docs). Always construct/renormalize through this type's API.
34#[derive(Clone)]
35pub enum AverInt {
36    /// A value that fits `i64` — the common, allocation-free case.
37    Small(i64),
38    /// A value outside the `i64` range. Boxed to keep the enum small (a
39    /// bare `BigInt` is three machine words; boxing keeps `AverInt` at two).
40    Big(Box<BigInt>),
41}
42
43impl AverInt {
44    /// The mathematical integer `n`, stored inline.
45    #[inline]
46    pub const fn from_i64(n: i64) -> Self {
47        AverInt::Small(n)
48    }
49
50    /// Zero.
51    #[inline]
52    pub const fn zero() -> Self {
53        AverInt::Small(0)
54    }
55
56    /// Renormalize a `BigInt` to canonical form: demote to `Small` when it
57    /// fits `i64`, otherwise box it as `Big`. Every path that produces or
58    /// reconstructs a `BigInt` must funnel through here so the canonical
59    /// invariant (a value fitting `i64` is *always* `Small`) holds — this is
60    /// the sole sanctioned way to build a `Big`, which is why `Big`'s payload
61    /// is private.
62    #[inline]
63    pub fn from_bigint(b: BigInt) -> Self {
64        match b.to_i64() {
65            Some(n) => AverInt::Small(n),
66            None => AverInt::Big(Box::new(b)),
67        }
68    }
69
70    /// Borrow as a `BigInt` for an operation that cannot stay in `i64`.
71    #[inline]
72    fn to_bigint(&self) -> BigInt {
73        match self {
74            AverInt::Small(n) => BigInt::from(*n),
75            AverInt::Big(b) => (**b).clone(),
76        }
77    }
78
79    /// `true` for the additive identity.
80    #[inline]
81    pub fn is_zero(&self) -> bool {
82        match self {
83            AverInt::Small(n) => *n == 0,
84            AverInt::Big(b) => b.is_zero(),
85        }
86    }
87
88    // -- Arithmetic (non-wrapping) ----------------------------------------
89    //
90    // Each operation takes the i64 fast path via `checked_*`; only on an
91    // i64-overflow does it promote to `BigInt`. The result is always
92    // renormalized, so e.g. `Big - Big` that cancels back into range returns
93    // `Small`.
94
95    /// `self + rhs` over ℤ (never wraps).
96    pub fn add(&self, rhs: &AverInt) -> AverInt {
97        if let (AverInt::Small(a), AverInt::Small(b)) = (self, rhs) {
98            if let Some(s) = a.checked_add(*b) {
99                return AverInt::Small(s);
100            }
101        }
102        AverInt::from_bigint(self.to_bigint() + rhs.to_bigint())
103    }
104
105    /// `self - rhs` over ℤ (never wraps).
106    pub fn sub(&self, rhs: &AverInt) -> AverInt {
107        if let (AverInt::Small(a), AverInt::Small(b)) = (self, rhs) {
108            if let Some(s) = a.checked_sub(*b) {
109                return AverInt::Small(s);
110            }
111        }
112        AverInt::from_bigint(self.to_bigint() - rhs.to_bigint())
113    }
114
115    /// `self * rhs` over ℤ (never wraps).
116    pub fn mul(&self, rhs: &AverInt) -> AverInt {
117        if let (AverInt::Small(a), AverInt::Small(b)) = (self, rhs) {
118            if let Some(s) = a.checked_mul(*b) {
119                return AverInt::Small(s);
120            }
121        }
122        AverInt::from_bigint(self.to_bigint() * rhs.to_bigint())
123    }
124
125    /// `-self` over ℤ (never wraps; `-i64::MIN` promotes to `Big`).
126    pub fn neg(&self) -> AverInt {
127        match self {
128            AverInt::Small(n) => match n.checked_neg() {
129                Some(v) => AverInt::Small(v),
130                None => AverInt::from_bigint(-BigInt::from(*n)),
131            },
132            AverInt::Big(b) => AverInt::from_bigint(-(**b).clone()),
133        }
134    }
135
136    /// Euclidean quotient, matching `i64::div_euclid` and the Lean/Dafny
137    /// `Int.ediv` model the proofs cite: the unique `q` with a remainder in
138    /// `[0, |rhs|)`. Returns `None` when `rhs == 0`. Over ℤ there is no
139    /// `i64::MIN / -1` overflow edge — it is just `i64::MAX + 1`, returned as
140    /// a `Big`.
141    pub fn div_euclid(&self, rhs: &AverInt) -> Option<AverInt> {
142        if rhs.is_zero() {
143            return None;
144        }
145        if let (AverInt::Small(a), AverInt::Small(b)) = (self, rhs) {
146            // `div_euclid` on i64 is total except for `MIN / -1`, which
147            // overflows; fall through to BigInt for that single edge.
148            if let Some(q) = a.checked_div_euclid(*b) {
149                return Some(AverInt::Small(q));
150            }
151        }
152        let (q, _r) = euclid_div_rem(&self.to_bigint(), &rhs.to_bigint());
153        Some(AverInt::from_bigint(q))
154    }
155
156    /// Euclidean remainder `self - rhs * div_euclid(self, rhs)`, matching
157    /// `i64::rem_euclid` and the Lean/Dafny `Int.emod` model. Returns `None`
158    /// when `rhs == 0`. The result is always non-negative and in `[0, |rhs|)`,
159    /// independent of the sign of either operand.
160    pub fn rem_euclid(&self, rhs: &AverInt) -> Option<AverInt> {
161        if rhs.is_zero() {
162            return None;
163        }
164        if let (AverInt::Small(a), AverInt::Small(b)) = (self, rhs) {
165            if let Some(r) = a.checked_rem_euclid(*b) {
166                return Some(AverInt::Small(r));
167            }
168        }
169        let (_q, r) = euclid_div_rem(&self.to_bigint(), &rhs.to_bigint());
170        Some(AverInt::from_bigint(r))
171    }
172
173    /// Truncating quotient (rounds toward zero), the semantics of the raw
174    /// `/` operator. Returns `None` when `rhs == 0`. Distinct from
175    /// `div_euclid` for negative operands; provided for the low-level
176    /// arithmetic opcodes (`Int.div` uses the Euclidean form).
177    pub fn div_trunc(&self, rhs: &AverInt) -> Option<AverInt> {
178        if rhs.is_zero() {
179            return None;
180        }
181        if let (AverInt::Small(a), AverInt::Small(b)) = (self, rhs) {
182            if let Some(q) = a.checked_div(*b) {
183                return Some(AverInt::Small(q));
184            }
185        }
186        Some(AverInt::from_bigint(self.to_bigint() / rhs.to_bigint()))
187    }
188
189    /// Truncating remainder (sign follows the dividend), the semantics of the
190    /// raw `%` operator. Returns `None` when `rhs == 0`.
191    pub fn rem_trunc(&self, rhs: &AverInt) -> Option<AverInt> {
192        if rhs.is_zero() {
193            return None;
194        }
195        if let (AverInt::Small(a), AverInt::Small(b)) = (self, rhs) {
196            if let Some(r) = a.checked_rem(*b) {
197                return Some(AverInt::Small(r));
198            }
199        }
200        Some(AverInt::from_bigint(self.to_bigint() % rhs.to_bigint()))
201    }
202
203    /// `|self|` over ℤ (never wraps; `|i64::MIN|` promotes to `Big`).
204    pub fn abs(&self) -> AverInt {
205        match self {
206            AverInt::Small(n) => match n.checked_abs() {
207                Some(v) => AverInt::Small(v),
208                None => AverInt::from_bigint(BigInt::from(*n).magnitude().clone().into()),
209            },
210            AverInt::Big(b) => AverInt::from_bigint(BigInt::from(b.magnitude().clone())),
211        }
212    }
213
214    /// The smaller of `self` and `other` (borrowing form, to avoid the
215    /// by-value `Ord::min`/`max` and keep the small-int clone cheap).
216    pub fn min_ref(&self, other: &AverInt) -> AverInt {
217        if self <= other {
218            self.clone()
219        } else {
220            other.clone()
221        }
222    }
223
224    /// The larger of `self` and `other`.
225    pub fn max_ref(&self, other: &AverInt) -> AverInt {
226        if self >= other {
227            self.clone()
228        } else {
229            other.clone()
230        }
231    }
232
233    // -- Checked conversions to machine integers --------------------------
234    //
235    // Every conversion to a fixed-width machine integer is checked: it
236    // returns `None` on out-of-range rather than wrapping or truncating.
237    // Callers at index/sentinel sites map `None` to the language's `None`;
238    // callers at capacity/host sites map `None` to a clean error.
239
240    /// `self` as `i64` if it fits, else `None`.
241    #[inline]
242    pub fn to_i64(&self) -> Option<i64> {
243        match self {
244            AverInt::Small(n) => Some(*n),
245            AverInt::Big(b) => b.to_i64(),
246        }
247    }
248
249    /// `self` as `usize` if it fits (non-negative and in range), else `None`.
250    #[inline]
251    pub fn to_usize(&self) -> Option<usize> {
252        match self {
253            AverInt::Small(n) => usize::try_from(*n).ok(),
254            AverInt::Big(b) => b.to_usize(),
255        }
256    }
257
258    /// `self` as `u16` if it fits, else `None`.
259    #[inline]
260    pub fn to_u16(&self) -> Option<u16> {
261        match self {
262            AverInt::Small(n) => u16::try_from(*n).ok(),
263            AverInt::Big(b) => b.to_u16(),
264        }
265    }
266
267    /// `self` as `u32` if it fits, else `None`.
268    #[inline]
269    pub fn to_u32(&self) -> Option<u32> {
270        match self {
271            AverInt::Small(n) => u32::try_from(*n).ok(),
272            AverInt::Big(b) => b.to_u32(),
273        }
274    }
275
276    /// `self` as `f64`, lossily. Huge magnitudes saturate to `±∞` (never
277    /// `NaN`), matching the Lean prelude's `Float.ofInt`/IEEE coercion. This
278    /// is the only intentionally-lossy conversion.
279    #[inline]
280    pub fn to_f64(&self) -> f64 {
281        match self {
282            AverInt::Small(n) => *n as f64,
283            // `BigInt::to_f64` returns `Some(±inf)` for out-of-range
284            // magnitudes and is never `None`, so the unwrap is total.
285            AverInt::Big(b) => b.to_f64().unwrap_or(f64::INFINITY),
286        }
287    }
288
289    /// Truncate a finite `f64` toward zero into ℤ. The exact mirror of the
290    /// VM's `float_to_aver_int` (`src/types/int.rs`): non-finite (`NaN`/`±∞`)
291    /// maps to `0`; an in-`i64`-range truncated value stays `Small`; an
292    /// out-of-range *finite* magnitude is represented EXACTLY as a `Big` via
293    /// `BigInt::from_f64`.
294    ///
295    /// This is the constructor `Int.fromFloat` and `Float.floor/ceil/round`
296    /// must funnel through — a bare `f as i64` cast SATURATES huge finite
297    /// floats to `i64::MAX`/`MIN` (a silent wrong value), which this avoids.
298    pub fn from_f64_trunc(f: f64) -> AverInt {
299        if !f.is_finite() {
300            return AverInt::zero();
301        }
302        let truncated = f.trunc();
303        match truncated.to_i64() {
304            Some(n) => AverInt::Small(n),
305            // Out of i64 range but finite: represent exactly via BigInt.
306            None => match BigInt::from_f64(truncated) {
307                Some(b) => AverInt::from_bigint(b),
308                None => AverInt::zero(),
309            },
310        }
311    }
312}
313
314/// Euclidean `(quotient, remainder)` for `BigInt` operands, value-identical to
315/// `i64::div_euclid` / `i64::rem_euclid` across every sign combination: the
316/// unique pair with `a == b*q + r` and `0 <= r < |b|`.
317///
318/// Note this is *not* num-integer's `div_floor`/`mod_floor`. Floored division
319/// gives the remainder the sign of the divisor, so it coincides with Euclidean
320/// only when `b > 0`; for `b < 0` a floored remainder is negative. We start
321/// from the truncating quotient/remainder (`div_rem`, remainder takes the
322/// dividend's sign) and, when that remainder is negative, step it into
323/// `[0, |b|)`: toward `b > 0` add `b` and drop the quotient by one; toward
324/// `b < 0` subtract `b` and raise the quotient by one.
325fn euclid_div_rem(a: &BigInt, b: &BigInt) -> (BigInt, BigInt) {
326    let (q, r) = a.div_rem(b);
327    if r.sign() == num_bigint::Sign::Minus {
328        if b.sign() == num_bigint::Sign::Plus {
329            (q - 1, r + b)
330        } else {
331            (q + 1, r - b)
332        }
333    } else {
334        (q, r)
335    }
336}
337
338// -- Equality / ordering / hashing -----------------------------------------
339//
340// The canonical-form invariant makes these total over representations: equal
341// numbers are always the same variant carrying the same payload.
342
343impl PartialEq for AverInt {
344    #[inline]
345    fn eq(&self, other: &Self) -> bool {
346        match (self, other) {
347            (AverInt::Small(a), AverInt::Small(b)) => a == b,
348            (AverInt::Big(a), AverInt::Big(b)) => a == b,
349            // Canonical form guarantees a Small and a Big are never equal.
350            _ => false,
351        }
352    }
353}
354
355impl Eq for AverInt {}
356
357impl Ord for AverInt {
358    fn cmp(&self, other: &Self) -> Ordering {
359        match (self, other) {
360            (AverInt::Small(a), AverInt::Small(b)) => a.cmp(b),
361            (AverInt::Big(a), AverInt::Big(b)) => a.cmp(b),
362            // A Big is out of i64 range, so its sign decides the comparison
363            // against any Small.
364            (AverInt::Small(_), AverInt::Big(b)) => {
365                if b.sign() == num_bigint::Sign::Minus {
366                    Ordering::Greater
367                } else {
368                    Ordering::Less
369                }
370            }
371            (AverInt::Big(a), AverInt::Small(_)) => {
372                if a.sign() == num_bigint::Sign::Minus {
373                    Ordering::Less
374                } else {
375                    Ordering::Greater
376                }
377            }
378        }
379    }
380}
381
382impl PartialOrd for AverInt {
383    #[inline]
384    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
385        Some(self.cmp(other))
386    }
387}
388
389impl core::hash::Hash for AverInt {
390    #[inline]
391    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
392        // Hash on the canonical numeric form: a Small and a numerically-equal
393        // Big can never coexist (canonical invariant), so hashing the variant
394        // payload is sufficient and consistent with `Eq`.
395        match self {
396            AverInt::Small(n) => n.hash(state),
397            AverInt::Big(b) => b.hash(state),
398        }
399    }
400}
401
402impl fmt::Display for AverInt {
403    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404        match self {
405            AverInt::Small(n) => write!(f, "{}", n),
406            AverInt::Big(b) => write!(f, "{}", b),
407        }
408    }
409}
410
411impl fmt::Debug for AverInt {
412    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
413        // Print the bare number so `Value::Int(..)`'s derived `Debug` reads
414        // `Int(42)` (not `Int(AverInt(42))`), keeping golden strings stable.
415        match self {
416            AverInt::Small(n) => write!(f, "{}", n),
417            AverInt::Big(b) => write!(f, "{}", b),
418        }
419    }
420}
421
422impl FromStr for AverInt {
423    type Err = ();
424
425    /// Parse a decimal integer of arbitrary length. Rejects empty/garbage
426    /// input (and anything `BigInt` rejects) with `Err(())`.
427    fn from_str(s: &str) -> Result<Self, Self::Err> {
428        // Fast path: most parsed ints fit i64.
429        if let Ok(n) = s.parse::<i64>() {
430            return Ok(AverInt::Small(n));
431        }
432        match BigInt::from_str(s) {
433            Ok(b) => Ok(AverInt::from_bigint(b)),
434            Err(_) => Err(()),
435        }
436    }
437}
438
439impl From<i64> for AverInt {
440    #[inline]
441    fn from(n: i64) -> Self {
442        AverInt::Small(n)
443    }
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449
450    fn big(s: &str) -> AverInt {
451        AverInt::from_str(s).unwrap()
452    }
453
454    #[test]
455    fn canonical_form_demotes_to_small() {
456        // i64::MAX * 2 / 2 returns to range and must be Small again.
457        let a = AverInt::from_i64(i64::MAX);
458        let doubled = a.add(&a);
459        assert!(matches!(doubled, AverInt::Big(_)));
460        let halved = doubled.div_euclid(&AverInt::from_i64(2)).unwrap();
461        assert_eq!(halved, AverInt::from_i64(i64::MAX));
462        assert!(matches!(halved, AverInt::Small(_)));
463    }
464
465    #[test]
466    fn square_is_non_negative_past_i64() {
467        // The exact runtime-vs-proof law: a*a >= 0 even where i64 would wrap.
468        let a = AverInt::from_i64(i64::MAX);
469        let sq = a.mul(&a);
470        assert!(matches!(sq, AverInt::Big(_)));
471        assert!(sq >= AverInt::zero());
472    }
473
474    #[test]
475    fn equal_bigs_built_differently_are_equal_and_hash_equal() {
476        use std::collections::hash_map::DefaultHasher;
477        use std::hash::{Hash, Hasher};
478        let big_a = AverInt::from_i64(i64::MAX).add(&AverInt::from_i64(1));
479        let big_b = big("9223372036854775808");
480        assert_eq!(big_a, big_b);
481        let mut ha = DefaultHasher::new();
482        let mut hb = DefaultHasher::new();
483        big_a.hash(&mut ha);
484        big_b.hash(&mut hb);
485        assert_eq!(ha.finish(), hb.finish());
486    }
487
488    #[test]
489    fn from_bigint_demotes_in_range_value_to_small() {
490        use std::collections::hash_map::DefaultHasher;
491        use std::hash::{Hash, Hasher};
492        // A BigInt that fits i64 must canonicalize to Small, so it compares
493        // AND hashes equal to the directly-built Small (the invariant Map/Set
494        // keys depend on). The public `from_bigint` is the only sanctioned way
495        // to build a Big, and it upholds this.
496        let from_big = AverInt::from_bigint(BigInt::from(5));
497        let small = AverInt::from_i64(5);
498        assert!(matches!(from_big, AverInt::Small(5)));
499        assert_eq!(from_big, small);
500        let mut hb = DefaultHasher::new();
501        let mut hs = DefaultHasher::new();
502        from_big.hash(&mut hb);
503        small.hash(&mut hs);
504        assert_eq!(hb.finish(), hs.finish());
505
506        // Boundary values that exactly fit i64 also demote.
507        assert!(matches!(
508            AverInt::from_bigint(BigInt::from(i64::MAX)),
509            AverInt::Small(i64::MAX)
510        ));
511        assert!(matches!(
512            AverInt::from_bigint(BigInt::from(i64::MIN)),
513            AverInt::Small(i64::MIN)
514        ));
515        // One past the boundary stays Big.
516        let past = AverInt::from_bigint(BigInt::from(i64::MAX) + 1);
517        assert!(matches!(past, AverInt::Big(_)));
518    }
519
520    #[test]
521    fn euclidean_div_mod_match_i64_in_range() {
522        for a in [-7i64, -1, 0, 1, 7, 100] {
523            for b in [-3i64, -1, 1, 3, 5] {
524                let ai = AverInt::from_i64(a);
525                let bi = AverInt::from_i64(b);
526                assert_eq!(
527                    ai.div_euclid(&bi).unwrap(),
528                    AverInt::from_i64(a.div_euclid(b))
529                );
530                assert_eq!(
531                    ai.rem_euclid(&bi).unwrap(),
532                    AverInt::from_i64(a.rem_euclid(b))
533                );
534            }
535        }
536    }
537
538    #[test]
539    fn euclidean_div_mod_big_branch_negative_divisor() {
540        // 2^63: the smallest magnitude that forces the Big fallback (no in-range
541        // i64 dividend reaches it). The exact case the i64 fast path can never
542        // exercise, so it pins the BigInt code directly.
543        let two_63 = AverInt::from_i64(i64::MAX).add(&AverInt::from_i64(1));
544        assert!(matches!(two_63, AverInt::Big(_)));
545
546        let neg3 = AverInt::from_i64(-3);
547        let q = two_63.div_euclid(&neg3).unwrap();
548        let r = two_63.rem_euclid(&neg3).unwrap();
549        // Euclidean: 2^63 = (-3)*(-3074457345618258602) + 2, with 0 <= 2 < 3.
550        assert_eq!(q, big("-3074457345618258602"));
551        assert_eq!(r, AverInt::from_i64(2));
552        assert!(r >= AverInt::zero());
553    }
554
555    #[test]
556    fn euclidean_div_mod_big_full_sign_matrix() {
557        // Force the Big branch on both operands across the whole sign matrix and
558        // assert the Euclidean contract: 0 <= r < |b| and a == b*q + r.
559        let pos = big("9223372036854775808"); // 2^63 (just past i64::MAX)
560        let neg = big("-9223372036854775809"); // -2^63 - 1 (just past i64::MIN)
561        let dpos = big("100000000000000000000"); // 10^20, Big divisor
562        let dneg = big("-100000000000000000000"); // -10^20
563
564        for a in [&pos, &neg] {
565            for b in [&dpos, &dneg] {
566                assert!(matches!(*a, AverInt::Big(_)));
567                assert!(matches!(*b, AverInt::Big(_)));
568                let q = a.div_euclid(b).unwrap();
569                let r = a.rem_euclid(b).unwrap();
570                // 0 <= r
571                assert!(r >= AverInt::zero(), "remainder negative for {a}/{b}");
572                // r < |b|
573                assert!(r < b.abs(), "remainder >= |divisor| for {a}/{b}");
574                // a == b*q + r
575                assert_eq!(&b.mul(&q).add(&r), a, "identity broken for {a}/{b}");
576            }
577        }
578    }
579
580    #[test]
581    fn div_by_zero_is_none() {
582        assert!(AverInt::from_i64(5).div_euclid(&AverInt::zero()).is_none());
583        assert!(AverInt::from_i64(5).rem_euclid(&AverInt::zero()).is_none());
584    }
585
586    #[test]
587    fn min_div_neg_one_promotes_not_panics() {
588        let min = AverInt::from_i64(i64::MIN);
589        let q = min.div_euclid(&AverInt::from_i64(-1)).unwrap();
590        assert!(matches!(q, AverInt::Big(_)));
591        assert_eq!(q, big("9223372036854775808"));
592    }
593
594    #[test]
595    fn abs_of_min_promotes() {
596        let q = AverInt::from_i64(i64::MIN).abs();
597        assert_eq!(q, big("9223372036854775808"));
598    }
599
600    #[test]
601    fn checked_conversions_reject_out_of_range() {
602        let huge = big("99999999999999999999999999");
603        assert_eq!(huge.to_i64(), None);
604        assert_eq!(huge.to_usize(), None);
605        assert_eq!(huge.to_u32(), None);
606        assert_eq!(huge.to_u16(), None);
607        assert_eq!(AverInt::from_i64(-1).to_usize(), None);
608        assert_eq!(AverInt::from_i64(70000).to_u16(), None);
609    }
610
611    #[test]
612    fn to_f64_saturates_to_infinity() {
613        let huge = big("1").mul(&big("10").mul(&big("10"))); // small, sanity
614        assert_eq!(huge.to_f64(), 100.0);
615        let enormous = AverInt::from_i64(10).mul(&AverInt::from_i64(10));
616        assert_eq!(enormous.to_f64(), 100.0);
617        // 10^400 overflows f64 -> +inf, never NaN.
618        let mut p = AverInt::from_i64(1);
619        let ten = AverInt::from_i64(10);
620        for _ in 0..400 {
621            p = p.mul(&ten);
622        }
623        assert!(p.to_f64().is_infinite() && p.to_f64() > 0.0);
624        assert!(p.neg().to_f64().is_infinite() && p.neg().to_f64() < 0.0);
625    }
626
627    #[test]
628    fn parse_roundtrip_past_i64() {
629        let s = "170141183460469231731687303715884105727"; // 2^127 - 1
630        let v = big(s);
631        assert!(matches!(v, AverInt::Big(_)));
632        assert_eq!(v.to_string(), s);
633    }
634
635    #[test]
636    fn from_str_rejects_garbage() {
637        assert!(AverInt::from_str("").is_err());
638        assert!(AverInt::from_str("12x").is_err());
639        assert!(AverInt::from_str("1.5").is_err());
640    }
641
642    #[test]
643    fn from_f64_trunc_preserves_huge_finite_magnitudes() {
644        // The fix #1 case: a float far past i64::MAX must NOT saturate to
645        // i64::MAX (`as i64`), but produce the EXACT BigInt — mirroring the
646        // VM's `float_to_aver_int`.
647        let v = AverInt::from_f64_trunc(1e20);
648        assert!(matches!(v, AverInt::Big(_)));
649        assert_eq!(v.to_string(), "100000000000000000000");
650        // Negative huge magnitude is exact too.
651        let n = AverInt::from_f64_trunc(-1e20);
652        assert_eq!(n.to_string(), "-100000000000000000000");
653    }
654
655    #[test]
656    fn from_f64_trunc_truncates_toward_zero_in_range() {
657        assert_eq!(AverInt::from_f64_trunc(3.9), AverInt::from_i64(3));
658        assert_eq!(AverInt::from_f64_trunc(-3.9), AverInt::from_i64(-3));
659        assert_eq!(AverInt::from_f64_trunc(0.0), AverInt::zero());
660    }
661
662    #[test]
663    fn from_f64_trunc_non_finite_is_zero() {
664        // NaN / ±∞ have no integer; map to 0 (matching the VM's cast).
665        assert_eq!(AverInt::from_f64_trunc(f64::NAN), AverInt::zero());
666        assert_eq!(AverInt::from_f64_trunc(f64::INFINITY), AverInt::zero());
667        assert_eq!(AverInt::from_f64_trunc(f64::NEG_INFINITY), AverInt::zero());
668    }
669}