Skip to main content

dashu_float/
fbig_cached.rs

1//! A cached floating-point number — [`FBig`] with a shared constant cache attached.
2
3use alloc::rc::Rc;
4use core::cell::RefCell;
5use core::cmp::Ordering;
6use core::str::FromStr;
7
8use dashu_base::{AbsOrd, ConversionError, EstimatedLog2, ParseError, Sign, Signed};
9use dashu_int::{IBig, UBig};
10
11use crate::error::panic_unlimited_precision;
12use crate::fbig::FBig;
13use crate::math::cache::ConstCache;
14use crate::repr::{Context, Repr, Word};
15use crate::round::{mode, Round, Rounded};
16use crate::utils::digit_len;
17
18/// A floating-point number that carries a shared handle to a [`ConstCache`].
19///
20/// It is functionally an [`FBig`]: same in-memory representation (`fbig`),
21/// plus an [`Rc<RefCell<ConstCache>>`] handle. The difference is that the
22/// transcendental operations (`ln`, `exp`, `sin`, `cos`, …, `pi`, base conversion)
23/// thread that handle into the underlying [`Context`] methods, so they reuse and
24/// progressively extend the cached exact binary-splitting state instead of
25/// recomputing constants from scratch on every call.
26///
27/// `Context`/`FBig` themselves stay `Copy` + `Send` + `Sync` + `no_std` (so
28/// `static_fbig!` keeps working); only this cached
29/// wrapper is `!Send + !Sync`, because it shares state through an `Rc<RefCell<..>>`.
30/// To share one cache across threads, build an analogous type over
31/// `Arc<Mutex<ConstCache>>` instead (the [`Context`] methods accept
32/// `Option<&mut ConstCache>`, independent of the container).
33///
34/// Every value-producing operation returns a `CachedFBig` that preserves the
35/// handle, so `(a + b).ln().exp()` stays cached throughout — no silent cache loss.
36/// When two `CachedFBig` values with different cache handles interact in a binary
37/// operation, the LHS (left-hand-side) cache is preserved in the result. For
38/// `FBig op CachedFBig`, the `CachedFBig` operand's cache is preserved.
39///
40/// # Examples
41///
42/// ```
43/// use core::cell::RefCell;
44/// use core::str::FromStr;
45/// use dashu_float::{CachedFBig, ConstCache, Context};
46/// use dashu_float::round::mode::HalfAway;
47/// use std::rc::Rc;
48///
49/// let cache = Rc::new(RefCell::new(ConstCache::new()));
50/// // build a cached decimal number 1.234
51/// let x = CachedFBig::<HalfAway, 10>::with_cache(
52///     dashu_float::Repr::new(1234.into(), -3),
53///     Context::new(50),
54/// );
55///
56/// // ln / exp reuse the same shared cache handle
57/// let _ = x.clone().ln().exp();
58/// ```
59pub struct CachedFBig<R: Round = mode::Zero, const B: Word = 2> {
60    pub(crate) fbig: FBig<R, B>,
61    pub(crate) cache: Rc<RefCell<ConstCache>>,
62}
63
64impl<R: Round, const B: Word> CachedFBig<R, B> {
65    /// Wrap an [`FBig`], sharing the given cache handle.
66    #[inline]
67    pub fn new(value: FBig<R, B>, cache: Rc<RefCell<ConstCache>>) -> Self {
68        Self { fbig: value, cache }
69    }
70
71    /// Build from raw parts, sharing the given cache handle.
72    #[inline]
73    pub fn from_repr(repr: Repr<B>, context: Context<R>, cache: Rc<RefCell<ConstCache>>) -> Self {
74        Self {
75            fbig: FBig::new(repr, context),
76            cache,
77        }
78    }
79
80    /// Build from raw parts with a fresh, exclusive cache.
81    #[inline]
82    pub fn with_cache(repr: Repr<B>, context: Context<R>) -> Self {
83        Self::from_repr(repr, context, Rc::new(RefCell::new(ConstCache::new())))
84    }
85
86    /// Build a `CachedFBig` from an [`FBig`] result, re-attaching this value's
87    /// shared cache handle (cloned cheaply via `Rc`).
88    #[inline]
89    pub(crate) fn from_fbig(fbig: FBig<R, B>, cache: &Rc<RefCell<ConstCache>>) -> Self {
90        Self {
91            fbig,
92            cache: Rc::clone(cache),
93        }
94    }
95
96    /// Borrow the inner [`FBig`].
97    #[inline]
98    pub fn as_fbig(&self) -> &FBig<R, B> {
99        &self.fbig
100    }
101
102    /// Drop the cache handle and return the underlying [`FBig`].
103    #[inline]
104    pub fn into_fbig(self) -> FBig<R, B> {
105        self.fbig
106    }
107
108    /// Borrow the shared constant cache immutably.
109    ///
110    /// Use this to inspect cache state, e.g. `cached.cache().total_terms()`.
111    #[inline]
112    pub fn cache(&self) -> impl core::ops::Deref<Target = ConstCache> + '_ {
113        self.cache.borrow()
114    }
115
116    /// Clear all cached constant state, freeing the underlying memory.
117    ///
118    /// The next transcendental operation will recompute constants from scratch.
119    #[inline]
120    pub fn clear_cache(&self) {
121        self.cache.borrow_mut().clear();
122    }
123
124    /// π at `precision` base-`B` digits, reusing/extending `cache`.
125    pub fn pi(precision: usize, cache: &Rc<RefCell<ConstCache>>) -> Self {
126        let fbig = {
127            let mut c = cache.borrow_mut();
128            Context::<R>::new(precision).pi::<B>(Some(&mut *c)).value()
129        };
130        Self::from_fbig(fbig, cache)
131    }
132
133    /// *e* (Euler's number) at `precision` base-`B` digits.
134    ///
135    /// Unlike [`pi`](Self::pi), *e* is not cached: it depends on no other constant
136    /// and is reused by no operation, so there is no shared state to thread. The
137    /// `cache` handle is attached only so the result is a [`CachedFBig`] whose
138    /// later transcendental ops still share a cache.
139    pub fn e(precision: usize, cache: &Rc<RefCell<ConstCache>>) -> Self {
140        let fbig = Context::<R>::new(precision).e::<B>().value();
141        Self::from_fbig(fbig, cache)
142    }
143
144    // ----- accessors -----
145
146    /// Maximum precision set for the number (see [`FBig::precision`]).
147    #[inline]
148    pub const fn precision(&self) -> usize {
149        self.fbig.context.precision
150    }
151
152    /// Number of significant digits (see [`FBig::digits`]).
153    #[inline]
154    pub fn digits(&self) -> usize {
155        self.fbig.repr.digits()
156    }
157
158    /// The associated context.
159    #[inline]
160    pub const fn context(&self) -> Context<R> {
161        self.fbig.context
162    }
163
164    /// The underlying representation.
165    #[inline]
166    pub const fn repr(&self) -> &Repr<B> {
167        &self.fbig.repr
168    }
169
170    /// Consume and return the underlying representation.
171    #[inline]
172    pub fn into_repr(self) -> Repr<B> {
173        self.fbig.repr
174    }
175
176    /// Sign of the number (see [`FBig::sign`]).
177    #[inline]
178    pub const fn sign(&self) -> Sign {
179        self.fbig.repr.sign()
180    }
181
182    /// Change precision, preserving the handle (see [`FBig::with_precision`]).
183    pub fn with_precision(&self, precision: usize) -> Rounded<Self> {
184        self.fbig
185            .clone()
186            .with_precision(precision)
187            .map(|f| Self::from_fbig(f, &self.cache))
188    }
189
190    /// Change rounding mode, preserving the handle (see [`FBig::with_rounding`]).
191    pub fn with_rounding<NewR: Round>(&self) -> CachedFBig<NewR, B> {
192        CachedFBig::from_fbig(self.fbig.clone().with_rounding::<NewR>(), &self.cache)
193    }
194}
195
196impl<R: Round, const B: Word> CachedFBig<R, B> {
197    /// ULP of the number (see [`FBig::ulp`]).
198    pub fn ulp(&self) -> Self {
199        if self.fbig.context.precision == 0 {
200            panic_unlimited_precision();
201        }
202        let repr = Repr {
203            significand: dashu_int::IBig::ONE,
204            exponent: self.fbig.repr.exponent + self.fbig.repr.digits() as isize
205                - self.fbig.context.precision as isize,
206        };
207        Self::from_repr(repr, self.fbig.context, Rc::clone(&self.cache))
208    }
209
210    /// Convert to an integer (see [`FBig::to_int`]).
211    pub fn to_int(&self) -> Rounded<dashu_int::IBig> {
212        self.fbig.clone().to_int()
213    }
214
215    /// Convert to `f32` (see [`FBig::to_f32`]).
216    pub fn to_f32(&self) -> Rounded<f32> {
217        self.fbig.clone().to_f32()
218    }
219
220    /// Convert to `f64` (see [`FBig::to_f64`]).
221    pub fn to_f64(&self) -> Rounded<f64> {
222        self.fbig.clone().to_f64()
223    }
224
225    /// Construct from significand + exponent, with a fresh cache (see [`FBig::from_parts`]).
226    pub fn from_parts(significand: dashu_int::IBig, exponent: isize) -> Self {
227        let precision = digit_len::<B>(&significand).max(1);
228        let repr = Repr::new(significand, exponent);
229        Self::with_cache(repr, Context::new(precision))
230    }
231}
232
233// ---------------------------------------------------------------------------
234// From / Into
235// ---------------------------------------------------------------------------
236
237impl<R: Round, const B: Word> From<FBig<R, B>> for CachedFBig<R, B> {
238    #[inline]
239    fn from(fbig: FBig<R, B>) -> Self {
240        Self::new(fbig, Rc::new(RefCell::new(ConstCache::new())))
241    }
242}
243
244impl<R: Round, const B: Word> From<CachedFBig<R, B>> for FBig<R, B> {
245    #[inline]
246    fn from(cached: CachedFBig<R, B>) -> Self {
247        cached.into_fbig()
248    }
249}
250
251impl<R: Round, const B: Word> FBig<R, B> {
252    /// Attach a shared cache handle, turning this [`FBig`] into a [`CachedFBig`].
253    #[inline]
254    pub fn into_cached(self, cache: Rc<RefCell<ConstCache>>) -> CachedFBig<R, B> {
255        CachedFBig::new(self, cache)
256    }
257}
258
259// ---------------------------------------------------------------------------
260// FromStr / From / TryFrom
261//
262// Construction from an external value (string, integer, primitive float) attaches
263// a *fresh* cache, exactly like `From<FBig>` above — there is no existing handle
264// to share, and `FromStr`/`TryFrom` have no parameter for one.
265// ---------------------------------------------------------------------------
266
267impl<R: Round, const B: Word> FromStr for CachedFBig<R, B> {
268    type Err = ParseError;
269
270    #[inline]
271    fn from_str(s: &str) -> Result<Self, ParseError> {
272        Ok(FBig::from_str(s)?.into())
273    }
274}
275
276impl<R: Round, const B: Word> From<UBig> for CachedFBig<R, B> {
277    #[inline]
278    fn from(n: UBig) -> Self {
279        FBig::from(n).into()
280    }
281}
282
283impl<R: Round, const B: Word> From<IBig> for CachedFBig<R, B> {
284    #[inline]
285    fn from(n: IBig) -> Self {
286        FBig::from(n).into()
287    }
288}
289
290macro_rules! impl_from_int_for_cached_fbig {
291    ($($t:ty)*) => {$(
292        impl<R: Round, const B: Word> From<$t> for CachedFBig<R, B> {
293            #[inline]
294            fn from(value: $t) -> Self {
295                FBig::from(value).into()
296            }
297        }
298    )*};
299}
300impl_from_int_for_cached_fbig!(u8 u16 u32 u64 u128 usize i8 i16 i32 i64 i128 isize);
301
302impl<R: Round> TryFrom<f32> for CachedFBig<R, 2> {
303    type Error = ConversionError;
304
305    #[inline]
306    fn try_from(value: f32) -> Result<Self, Self::Error> {
307        FBig::try_from(value).map(Self::from)
308    }
309}
310
311impl<R: Round> TryFrom<f64> for CachedFBig<R, 2> {
312    type Error = ConversionError;
313
314    #[inline]
315    fn try_from(value: f64) -> Result<Self, Self::Error> {
316        FBig::try_from(value).map(Self::from)
317    }
318}
319
320macro_rules! impl_try_from_cached_fbig_for_int {
321    ($($t:ty)*) => {$(
322        impl<R: Round, const B: Word> TryFrom<CachedFBig<R, B>> for $t {
323            type Error = ConversionError;
324
325            #[inline]
326            fn try_from(value: CachedFBig<R, B>) -> Result<Self, Self::Error> {
327                value.fbig.try_into()
328            }
329        }
330    )*};
331}
332impl_try_from_cached_fbig_for_int!(
333    u8 u16 u32 u64 u128 usize i8 i16 i32 i64 i128 isize UBig IBig
334);
335
336impl<R: Round> TryFrom<CachedFBig<R, 2>> for f32 {
337    type Error = ConversionError;
338
339    #[inline]
340    fn try_from(value: CachedFBig<R, 2>) -> Result<Self, Self::Error> {
341        value.fbig.try_into()
342    }
343}
344
345impl<R: Round> TryFrom<CachedFBig<R, 2>> for f64 {
346    type Error = ConversionError;
347
348    #[inline]
349    fn try_from(value: CachedFBig<R, 2>) -> Result<Self, Self::Error> {
350        value.fbig.try_into()
351    }
352}
353
354// ---------------------------------------------------------------------------
355// Clone / Default / Debug / comparisons
356// ---------------------------------------------------------------------------
357
358impl<R: Round, const B: Word> Clone for CachedFBig<R, B> {
359    #[inline]
360    fn clone(&self) -> Self {
361        Self {
362            fbig: self.fbig.clone(),
363            cache: Rc::clone(&self.cache),
364        }
365    }
366}
367
368impl<R: Round, const B: Word> Default for CachedFBig<R, B> {
369    /// Default value: 0 with a fresh cache.
370    #[inline]
371    fn default() -> Self {
372        Self::with_cache(Repr::zero(), Context::new(0))
373    }
374}
375
376impl<R: Round, const B: Word> core::fmt::Debug for CachedFBig<R, B> {
377    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
378        f.debug_struct("CachedFBig")
379            .field("repr", &self.fbig.repr)
380            .field("precision", &self.fbig.context.precision)
381            .finish()
382    }
383}
384
385// ---------------------------------------------------------------------------
386// Display / LowerExp / UpperExp / base-specific formatting
387//
388// Each delegates to the inner FBig so the rendered string is identical to FBig.
389// (`Debug` above intentionally keeps the cached-specific struct form.)
390// ---------------------------------------------------------------------------
391
392impl<R: Round, const B: Word> core::fmt::Display for CachedFBig<R, B> {
393    #[inline]
394    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
395        core::fmt::Display::fmt(&self.fbig, f)
396    }
397}
398
399impl<R: Round, const B: Word> core::fmt::LowerExp for CachedFBig<R, B> {
400    #[inline]
401    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
402        core::fmt::LowerExp::fmt(&self.fbig, f)
403    }
404}
405
406impl<R: Round, const B: Word> core::fmt::UpperExp for CachedFBig<R, B> {
407    #[inline]
408    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
409        core::fmt::UpperExp::fmt(&self.fbig, f)
410    }
411}
412
413/// Mirror the base-specific format traits ([`core::fmt::Binary`], [`Octal`](core::fmt::Octal),
414/// [`LowerHex`](core::fmt::LowerHex)/[`UpperHex`](core::fmt::UpperHex)) onto [`CachedFBig`] for
415/// the bases where they
416/// apply, delegating each to the inner [`FBig`]'s impl so the output matches.
417macro_rules! impl_cached_fmt_with_base {
418    ($base:literal, $trait:ident) => {
419        impl<R: Round> core::fmt::$trait for CachedFBig<R, $base> {
420            #[inline]
421            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
422                core::fmt::$trait::fmt(&self.fbig, f)
423            }
424        }
425    };
426}
427impl_cached_fmt_with_base!(2, Binary);
428impl_cached_fmt_with_base!(2, LowerHex);
429impl_cached_fmt_with_base!(2, UpperHex);
430impl_cached_fmt_with_base!(8, Octal);
431impl_cached_fmt_with_base!(16, LowerHex);
432impl_cached_fmt_with_base!(16, UpperHex);
433
434impl<R1: Round, R2: Round, const B: Word> PartialEq<CachedFBig<R2, B>> for CachedFBig<R1, B> {
435    #[inline]
436    fn eq(&self, other: &CachedFBig<R2, B>) -> bool {
437        // value equality, mirroring FBig (compares the representation only).
438        self.fbig.repr == other.fbig.repr
439    }
440}
441
442impl<R: Round, const B: Word> Eq for CachedFBig<R, B> {}
443
444// ---------------------------------------------------------------------------
445// Ordering and the dashu-base ordering/log/sign traits
446// (delegate to the inner FBig — value ordering, context ignored)
447// ---------------------------------------------------------------------------
448
449impl<R1: Round, R2: Round, const B: Word> PartialOrd<CachedFBig<R2, B>> for CachedFBig<R1, B> {
450    #[inline]
451    fn partial_cmp(&self, other: &CachedFBig<R2, B>) -> Option<Ordering> {
452        self.fbig.partial_cmp(&other.fbig)
453    }
454}
455
456impl<R: Round, const B: Word> Ord for CachedFBig<R, B> {
457    #[inline]
458    fn cmp(&self, other: &Self) -> Ordering {
459        self.fbig.cmp(&other.fbig)
460    }
461}
462
463impl<R: Round, const B: Word> AbsOrd for CachedFBig<R, B> {
464    #[inline]
465    fn abs_cmp(&self, other: &Self) -> Ordering {
466        AbsOrd::abs_cmp(&self.fbig, &other.fbig)
467    }
468}
469
470impl<R: Round, const B: Word> AbsOrd<UBig> for CachedFBig<R, B> {
471    #[inline]
472    fn abs_cmp(&self, other: &UBig) -> Ordering {
473        AbsOrd::abs_cmp(&self.fbig, other)
474    }
475}
476impl<R: Round, const B: Word> AbsOrd<CachedFBig<R, B>> for UBig {
477    #[inline]
478    fn abs_cmp(&self, other: &CachedFBig<R, B>) -> Ordering {
479        AbsOrd::abs_cmp(self, &other.fbig)
480    }
481}
482impl<R: Round, const B: Word> AbsOrd<IBig> for CachedFBig<R, B> {
483    #[inline]
484    fn abs_cmp(&self, other: &IBig) -> Ordering {
485        AbsOrd::abs_cmp(&self.fbig, other)
486    }
487}
488impl<R: Round, const B: Word> AbsOrd<CachedFBig<R, B>> for IBig {
489    #[inline]
490    fn abs_cmp(&self, other: &CachedFBig<R, B>) -> Ordering {
491        AbsOrd::abs_cmp(self, &other.fbig)
492    }
493}
494
495impl<R: Round, const B: Word> Signed for CachedFBig<R, B> {
496    #[inline]
497    fn sign(&self) -> Sign {
498        self.fbig.sign()
499    }
500}
501
502impl<R: Round, const B: Word> EstimatedLog2 for CachedFBig<R, B> {
503    #[inline]
504    fn log2_bounds(&self) -> (f32, f32) {
505        EstimatedLog2::log2_bounds(&self.fbig)
506    }
507
508    #[inline]
509    fn log2_est(&self) -> f32 {
510        EstimatedLog2::log2_est(&self.fbig)
511    }
512}
513
514#[cfg(test)]
515mod tests {
516    use super::*;
517    use crate::round::mode;
518    use alloc::format;
519
520    fn handle() -> Rc<RefCell<ConstCache>> {
521        Rc::new(RefCell::new(ConstCache::new()))
522    }
523
524    /// An `FBig` with value `n` at the given precision (so inexact results match the
525    /// `CachedFBig` operands built at the same precision).
526    fn fbig(n: i32, prec: usize) -> FBig<mode::HalfAway, 10> {
527        FBig::from_repr(Repr::new(n.into(), 0), Context::new(prec))
528    }
529
530    #[test]
531    fn test_pi_matches_fbig() {
532        for &precision in &[10usize, 50, 100] {
533            let h = handle();
534            let cached = CachedFBig::<mode::HalfAway, 10>::pi(precision, &h).into_fbig();
535            let direct = FBig::<mode::HalfAway, 10>::pi(precision);
536            assert_eq!(cached, direct, "pi mismatch at precision {precision}");
537        }
538    }
539
540    #[test]
541    fn test_transcendentals_match_fbig() {
542        let x = CachedFBig::<mode::HalfAway, 10>::with_cache(
543            Repr::new(1234.into(), -3), // 1.234
544            Context::new(50),
545        );
546        let y = FBig::<mode::HalfAway, 10>::from_repr(Repr::new(1234.into(), -3), Context::new(50));
547
548        assert_eq!(x.clone().ln().into_fbig(), y.clone().ln());
549        assert_eq!(x.clone().exp().into_fbig(), y.clone().exp());
550        assert_eq!(x.clone().sin().into_fbig(), y.clone().sin());
551        assert_eq!(x.clone().cos().into_fbig(), y.clone().cos());
552        assert_eq!(x.clone().exp_m1().into_fbig(), y.clone().exp_m1());
553        assert_eq!(x.clone().ln_1p().into_fbig(), y.clone().ln_1p());
554        assert_eq!(x.clone().log2().into_fbig(), y.clone().log2());
555        assert_eq!(x.clone().log10().into_fbig(), y.clone().log10());
556        assert_eq!(x.powf(&x.clone()).into_fbig(), y.clone().powf(&y));
557    }
558
559    #[test]
560    fn test_cache_extension_matches_scratch() {
561        // Extending π 100 -> 1000 through one shared handle must equal a from-scratch compute.
562        let h = handle();
563        let _pi_100 = CachedFBig::<mode::HalfAway, 10>::pi(100, &h);
564        let pi_1000 = CachedFBig::<mode::HalfAway, 10>::pi(1000, &h).into_fbig();
565        let direct = Context::<mode::HalfAway>::new(1000).pi::<10>(None).value();
566        assert_eq!(pi_1000, direct);
567    }
568
569    #[test]
570    fn test_cache_survives_arithmetic() {
571        // a and b share one cache handle; the sum must keep it so the subsequent
572        // ln() reuses the same shared cache.
573        let h = handle();
574        let a = CachedFBig::<mode::HalfAway, 10>::from_repr(
575            Repr::new(2.into(), 0),
576            Context::new(30),
577            h.clone(),
578        );
579        let b = CachedFBig::<mode::HalfAway, 10>::from_repr(
580            Repr::new(3.into(), 0),
581            Context::new(30),
582            h.clone(),
583        );
584        let sum_ln = (a.clone() + b.clone()).ln().into_fbig();
585        let expected = (fbig(2, 30) + fbig(3, 30)).ln();
586        assert_eq!(sum_ln, expected);
587    }
588
589    #[test]
590    fn test_arithmetic_matches_fbig() {
591        let a =
592            CachedFBig::<mode::HalfAway, 10>::with_cache(Repr::new(2.into(), 0), Context::new(20));
593        let b =
594            CachedFBig::<mode::HalfAway, 10>::with_cache(Repr::new(3.into(), 0), Context::new(20));
595
596        assert_eq!((a.clone() + b.clone()).into_fbig(), fbig(2, 20) + fbig(3, 20));
597        assert_eq!((a.clone() - b.clone()).into_fbig(), fbig(2, 20) - fbig(3, 20));
598        assert_eq!((a.clone() * b.clone()).into_fbig(), fbig(2, 20) * fbig(3, 20));
599        assert_eq!((a.clone() / b.clone()).into_fbig(), fbig(2, 20) / fbig(3, 20));
600    }
601
602    #[test]
603    fn test_debug_compiles() {
604        let x = CachedFBig::<mode::HalfAway, 10>::with_cache(
605            Repr::new(1234.into(), -3),
606            Context::new(50),
607        );
608        let s = format!("{:?}", x);
609        assert!(s.contains("CachedFBig"));
610    }
611
612    #[test]
613    fn test_arithmetic_with_fbig() {
614        let a =
615            CachedFBig::<mode::HalfAway, 10>::with_cache(Repr::new(2.into(), 0), Context::new(20));
616        let b = fbig(3, 20);
617
618        // CachedFBig op FBig — cache preserved (LHS)
619        let c = a.clone() + b.clone();
620        assert_eq!(c.into_fbig(), fbig(2, 20) + fbig(3, 20));
621
622        // FBig op CachedFBig — cache preserved (RHS)
623        let d = b.clone() + a.clone();
624        assert_eq!(d.into_fbig(), fbig(3, 20) + fbig(2, 20));
625
626        // Sub, Mul, Div
627        assert_eq!((a.clone() - b.clone()).into_fbig(), fbig(2, 20) - fbig(3, 20));
628        assert_eq!((a.clone() * b.clone()).into_fbig(), fbig(2, 20) * fbig(3, 20));
629        assert_eq!((a.clone() / b.clone()).into_fbig(), fbig(2, 20) / fbig(3, 20));
630    }
631
632    #[test]
633    fn test_arithmetic_with_primitives() {
634        let a =
635            CachedFBig::<mode::HalfAway, 10>::with_cache(Repr::new(2.into(), 0), Context::new(20));
636
637        // CachedFBig op primitive
638        assert_eq!((a.clone() + 3u8).into_fbig(), fbig(2, 20) + 3u8);
639        assert_eq!((a.clone() - 3i32).into_fbig(), fbig(2, 20) - 3i32);
640        assert_eq!((a.clone() * 4u64).into_fbig(), fbig(2, 20) * 4u64);
641
642        // Primitive op CachedFBig
643        assert_eq!((3u8 + a.clone()).into_fbig(), 3u8 + fbig(2, 20));
644        assert_eq!((10i32 - a.clone()).into_fbig(), 10i32 - fbig(2, 20));
645    }
646
647    #[test]
648    fn test_cache_size() {
649        let x = CachedFBig::<mode::HalfAway, 10>::with_cache(
650            Repr::new(1234.into(), -3),
651            Context::new(50),
652        );
653        let _ = x.ln();
654        // After computing ln(1.234), the cache should have some state
655        assert!(x.cache().total_terms() > 0);
656        assert!(x.cache().total_words() > 0);
657    }
658
659    #[test]
660    fn test_cache_clear() {
661        let x = CachedFBig::<mode::HalfAway, 10>::with_cache(
662            Repr::new(1234.into(), -3),
663            Context::new(50),
664        );
665        let before_clear = x.ln().into_fbig();
666        assert!(x.cache().total_terms() > 0);
667
668        x.clear_cache();
669        assert_eq!(x.cache().total_terms(), 0);
670        assert_eq!(x.cache().total_words(), 0);
671
672        // After clearing, recomputation still produces the same result
673        let after_clear = x.ln().into_fbig();
674        assert_eq!(after_clear, before_clear);
675    }
676}