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    // ----- accessors -----
134
135    /// Maximum precision set for the number (see [`FBig::precision`]).
136    #[inline]
137    pub const fn precision(&self) -> usize {
138        self.fbig.context.precision
139    }
140
141    /// Number of significant digits (see [`FBig::digits`]).
142    #[inline]
143    pub fn digits(&self) -> usize {
144        self.fbig.repr.digits()
145    }
146
147    /// The associated context.
148    #[inline]
149    pub const fn context(&self) -> Context<R> {
150        self.fbig.context
151    }
152
153    /// The underlying representation.
154    #[inline]
155    pub const fn repr(&self) -> &Repr<B> {
156        &self.fbig.repr
157    }
158
159    /// Consume and return the underlying representation.
160    #[inline]
161    pub fn into_repr(self) -> Repr<B> {
162        self.fbig.repr
163    }
164
165    /// Sign of the number (see [`FBig::sign`]).
166    #[inline]
167    pub const fn sign(&self) -> Sign {
168        self.fbig.repr.sign()
169    }
170
171    /// Change precision, preserving the handle (see [`FBig::with_precision`]).
172    pub fn with_precision(&self, precision: usize) -> Rounded<Self> {
173        self.fbig
174            .clone()
175            .with_precision(precision)
176            .map(|f| Self::from_fbig(f, &self.cache))
177    }
178
179    /// Change rounding mode, preserving the handle (see [`FBig::with_rounding`]).
180    pub fn with_rounding<NewR: Round>(&self) -> CachedFBig<NewR, B> {
181        CachedFBig::from_fbig(self.fbig.clone().with_rounding::<NewR>(), &self.cache)
182    }
183}
184
185impl<R: Round, const B: Word> CachedFBig<R, B> {
186    /// ULP of the number (see [`FBig::ulp`]).
187    pub fn ulp(&self) -> Self {
188        if self.fbig.context.precision == 0 {
189            panic_unlimited_precision();
190        }
191        let repr = Repr {
192            significand: dashu_int::IBig::ONE,
193            exponent: self.fbig.repr.exponent + self.fbig.repr.digits() as isize
194                - self.fbig.context.precision as isize,
195        };
196        Self::from_repr(repr, self.fbig.context, Rc::clone(&self.cache))
197    }
198
199    /// Convert to an integer (see [`FBig::to_int`]).
200    pub fn to_int(&self) -> Rounded<dashu_int::IBig> {
201        self.fbig.clone().to_int()
202    }
203
204    /// Convert to `f32` (see [`FBig::to_f32`]).
205    pub fn to_f32(&self) -> Rounded<f32> {
206        self.fbig.clone().to_f32()
207    }
208
209    /// Convert to `f64` (see [`FBig::to_f64`]).
210    pub fn to_f64(&self) -> Rounded<f64> {
211        self.fbig.clone().to_f64()
212    }
213
214    /// Construct from significand + exponent, with a fresh cache (see [`FBig::from_parts`]).
215    pub fn from_parts(significand: dashu_int::IBig, exponent: isize) -> Self {
216        let precision = digit_len::<B>(&significand).max(1);
217        let repr = Repr::new(significand, exponent);
218        Self::with_cache(repr, Context::new(precision))
219    }
220}
221
222// ---------------------------------------------------------------------------
223// From / Into
224// ---------------------------------------------------------------------------
225
226impl<R: Round, const B: Word> From<FBig<R, B>> for CachedFBig<R, B> {
227    #[inline]
228    fn from(fbig: FBig<R, B>) -> Self {
229        Self::new(fbig, Rc::new(RefCell::new(ConstCache::new())))
230    }
231}
232
233impl<R: Round, const B: Word> From<CachedFBig<R, B>> for FBig<R, B> {
234    #[inline]
235    fn from(cached: CachedFBig<R, B>) -> Self {
236        cached.into_fbig()
237    }
238}
239
240impl<R: Round, const B: Word> FBig<R, B> {
241    /// Attach a shared cache handle, turning this [`FBig`] into a [`CachedFBig`].
242    #[inline]
243    pub fn into_cached(self, cache: Rc<RefCell<ConstCache>>) -> CachedFBig<R, B> {
244        CachedFBig::new(self, cache)
245    }
246}
247
248// ---------------------------------------------------------------------------
249// FromStr / From / TryFrom
250//
251// Construction from an external value (string, integer, primitive float) attaches
252// a *fresh* cache, exactly like `From<FBig>` above — there is no existing handle
253// to share, and `FromStr`/`TryFrom` have no parameter for one.
254// ---------------------------------------------------------------------------
255
256impl<R: Round, const B: Word> FromStr for CachedFBig<R, B> {
257    type Err = ParseError;
258
259    #[inline]
260    fn from_str(s: &str) -> Result<Self, ParseError> {
261        Ok(FBig::from_str(s)?.into())
262    }
263}
264
265impl<R: Round, const B: Word> From<UBig> for CachedFBig<R, B> {
266    #[inline]
267    fn from(n: UBig) -> Self {
268        FBig::from(n).into()
269    }
270}
271
272impl<R: Round, const B: Word> From<IBig> for CachedFBig<R, B> {
273    #[inline]
274    fn from(n: IBig) -> Self {
275        FBig::from(n).into()
276    }
277}
278
279macro_rules! impl_from_int_for_cached_fbig {
280    ($($t:ty)*) => {$(
281        impl<R: Round, const B: Word> From<$t> for CachedFBig<R, B> {
282            #[inline]
283            fn from(value: $t) -> Self {
284                FBig::from(value).into()
285            }
286        }
287    )*};
288}
289impl_from_int_for_cached_fbig!(u8 u16 u32 u64 u128 usize i8 i16 i32 i64 i128 isize);
290
291impl<R: Round> TryFrom<f32> for CachedFBig<R, 2> {
292    type Error = ConversionError;
293
294    #[inline]
295    fn try_from(value: f32) -> Result<Self, Self::Error> {
296        FBig::try_from(value).map(Self::from)
297    }
298}
299
300impl<R: Round> TryFrom<f64> for CachedFBig<R, 2> {
301    type Error = ConversionError;
302
303    #[inline]
304    fn try_from(value: f64) -> Result<Self, Self::Error> {
305        FBig::try_from(value).map(Self::from)
306    }
307}
308
309macro_rules! impl_try_from_cached_fbig_for_int {
310    ($($t:ty)*) => {$(
311        impl<R: Round, const B: Word> TryFrom<CachedFBig<R, B>> for $t {
312            type Error = ConversionError;
313
314            #[inline]
315            fn try_from(value: CachedFBig<R, B>) -> Result<Self, Self::Error> {
316                value.fbig.try_into()
317            }
318        }
319    )*};
320}
321impl_try_from_cached_fbig_for_int!(
322    u8 u16 u32 u64 u128 usize i8 i16 i32 i64 i128 isize UBig IBig
323);
324
325impl<R: Round> TryFrom<CachedFBig<R, 2>> for f32 {
326    type Error = ConversionError;
327
328    #[inline]
329    fn try_from(value: CachedFBig<R, 2>) -> Result<Self, Self::Error> {
330        value.fbig.try_into()
331    }
332}
333
334impl<R: Round> TryFrom<CachedFBig<R, 2>> for f64 {
335    type Error = ConversionError;
336
337    #[inline]
338    fn try_from(value: CachedFBig<R, 2>) -> Result<Self, Self::Error> {
339        value.fbig.try_into()
340    }
341}
342
343// ---------------------------------------------------------------------------
344// Clone / Default / Debug / comparisons
345// ---------------------------------------------------------------------------
346
347impl<R: Round, const B: Word> Clone for CachedFBig<R, B> {
348    #[inline]
349    fn clone(&self) -> Self {
350        Self {
351            fbig: self.fbig.clone(),
352            cache: Rc::clone(&self.cache),
353        }
354    }
355}
356
357impl<R: Round, const B: Word> Default for CachedFBig<R, B> {
358    /// Default value: 0 with a fresh cache.
359    #[inline]
360    fn default() -> Self {
361        Self::with_cache(Repr::zero(), Context::new(0))
362    }
363}
364
365impl<R: Round, const B: Word> core::fmt::Debug for CachedFBig<R, B> {
366    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
367        f.debug_struct("CachedFBig")
368            .field("repr", &self.fbig.repr)
369            .field("precision", &self.fbig.context.precision)
370            .finish()
371    }
372}
373
374// ---------------------------------------------------------------------------
375// Display / LowerExp / UpperExp / base-specific formatting
376//
377// Each delegates to the inner FBig so the rendered string is identical to FBig.
378// (`Debug` above intentionally keeps the cached-specific struct form.)
379// ---------------------------------------------------------------------------
380
381impl<R: Round, const B: Word> core::fmt::Display for CachedFBig<R, B> {
382    #[inline]
383    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
384        core::fmt::Display::fmt(&self.fbig, f)
385    }
386}
387
388impl<R: Round, const B: Word> core::fmt::LowerExp for CachedFBig<R, B> {
389    #[inline]
390    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
391        core::fmt::LowerExp::fmt(&self.fbig, f)
392    }
393}
394
395impl<R: Round, const B: Word> core::fmt::UpperExp for CachedFBig<R, B> {
396    #[inline]
397    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
398        core::fmt::UpperExp::fmt(&self.fbig, f)
399    }
400}
401
402/// Mirror the base-specific format traits ([`core::fmt::Binary`], [`Octal`](core::fmt::Octal),
403/// [`LowerHex`]/[`UpperHex`](core::fmt::UpperHex)) onto [`CachedFBig`] for the bases where they
404/// apply, delegating each to the inner [`FBig`]'s impl so the output matches.
405macro_rules! impl_cached_fmt_with_base {
406    ($base:literal, $trait:ident) => {
407        impl<R: Round> core::fmt::$trait for CachedFBig<R, $base> {
408            #[inline]
409            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
410                core::fmt::$trait::fmt(&self.fbig, f)
411            }
412        }
413    };
414}
415impl_cached_fmt_with_base!(2, Binary);
416impl_cached_fmt_with_base!(2, LowerHex);
417impl_cached_fmt_with_base!(2, UpperHex);
418impl_cached_fmt_with_base!(8, Octal);
419impl_cached_fmt_with_base!(16, LowerHex);
420impl_cached_fmt_with_base!(16, UpperHex);
421
422impl<R1: Round, R2: Round, const B: Word> PartialEq<CachedFBig<R2, B>> for CachedFBig<R1, B> {
423    #[inline]
424    fn eq(&self, other: &CachedFBig<R2, B>) -> bool {
425        // value equality, mirroring FBig (compares the representation only).
426        self.fbig.repr == other.fbig.repr
427    }
428}
429
430impl<R: Round, const B: Word> Eq for CachedFBig<R, B> {}
431
432// ---------------------------------------------------------------------------
433// Ordering and the dashu-base ordering/log/sign traits
434// (delegate to the inner FBig — value ordering, context ignored)
435// ---------------------------------------------------------------------------
436
437impl<R1: Round, R2: Round, const B: Word> PartialOrd<CachedFBig<R2, B>> for CachedFBig<R1, B> {
438    #[inline]
439    fn partial_cmp(&self, other: &CachedFBig<R2, B>) -> Option<Ordering> {
440        self.fbig.partial_cmp(&other.fbig)
441    }
442}
443
444impl<R: Round, const B: Word> Ord for CachedFBig<R, B> {
445    #[inline]
446    fn cmp(&self, other: &Self) -> Ordering {
447        self.fbig.cmp(&other.fbig)
448    }
449}
450
451impl<R: Round, const B: Word> AbsOrd for CachedFBig<R, B> {
452    #[inline]
453    fn abs_cmp(&self, other: &Self) -> Ordering {
454        AbsOrd::abs_cmp(&self.fbig, &other.fbig)
455    }
456}
457
458impl<R: Round, const B: Word> AbsOrd<UBig> for CachedFBig<R, B> {
459    #[inline]
460    fn abs_cmp(&self, other: &UBig) -> Ordering {
461        AbsOrd::abs_cmp(&self.fbig, other)
462    }
463}
464impl<R: Round, const B: Word> AbsOrd<CachedFBig<R, B>> for UBig {
465    #[inline]
466    fn abs_cmp(&self, other: &CachedFBig<R, B>) -> Ordering {
467        AbsOrd::abs_cmp(self, &other.fbig)
468    }
469}
470impl<R: Round, const B: Word> AbsOrd<IBig> for CachedFBig<R, B> {
471    #[inline]
472    fn abs_cmp(&self, other: &IBig) -> Ordering {
473        AbsOrd::abs_cmp(&self.fbig, other)
474    }
475}
476impl<R: Round, const B: Word> AbsOrd<CachedFBig<R, B>> for IBig {
477    #[inline]
478    fn abs_cmp(&self, other: &CachedFBig<R, B>) -> Ordering {
479        AbsOrd::abs_cmp(self, &other.fbig)
480    }
481}
482
483impl<R: Round, const B: Word> Signed for CachedFBig<R, B> {
484    #[inline]
485    fn sign(&self) -> Sign {
486        self.fbig.sign()
487    }
488}
489
490impl<R: Round, const B: Word> EstimatedLog2 for CachedFBig<R, B> {
491    #[inline]
492    fn log2_bounds(&self) -> (f32, f32) {
493        EstimatedLog2::log2_bounds(&self.fbig)
494    }
495
496    #[inline]
497    fn log2_est(&self) -> f32 {
498        EstimatedLog2::log2_est(&self.fbig)
499    }
500}
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505    use crate::round::mode;
506    use alloc::format;
507
508    fn handle() -> Rc<RefCell<ConstCache>> {
509        Rc::new(RefCell::new(ConstCache::new()))
510    }
511
512    /// An `FBig` with value `n` at the given precision (so inexact results match the
513    /// `CachedFBig` operands built at the same precision).
514    fn fbig(n: i32, prec: usize) -> FBig<mode::HalfAway, 10> {
515        FBig::from_repr(Repr::new(n.into(), 0), Context::new(prec))
516    }
517
518    #[test]
519    fn test_pi_matches_fbig() {
520        for &precision in &[10usize, 50, 100] {
521            let h = handle();
522            let cached = CachedFBig::<mode::HalfAway, 10>::pi(precision, &h).into_fbig();
523            let direct = FBig::<mode::HalfAway, 10>::pi(precision);
524            assert_eq!(cached, direct, "pi mismatch at precision {precision}");
525        }
526    }
527
528    #[test]
529    fn test_transcendentals_match_fbig() {
530        let x = CachedFBig::<mode::HalfAway, 10>::with_cache(
531            Repr::new(1234.into(), -3), // 1.234
532            Context::new(50),
533        );
534        let y = FBig::<mode::HalfAway, 10>::from_repr(Repr::new(1234.into(), -3), Context::new(50));
535
536        assert_eq!(x.clone().ln().into_fbig(), y.clone().ln());
537        assert_eq!(x.clone().exp().into_fbig(), y.clone().exp());
538        assert_eq!(x.clone().sin().into_fbig(), y.clone().sin());
539        assert_eq!(x.clone().cos().into_fbig(), y.clone().cos());
540        assert_eq!(x.clone().exp_m1().into_fbig(), y.clone().exp_m1());
541        assert_eq!(x.clone().ln_1p().into_fbig(), y.clone().ln_1p());
542        assert_eq!(x.powf(&x.clone()).into_fbig(), y.clone().powf(&y));
543    }
544
545    #[test]
546    fn test_cache_extension_matches_scratch() {
547        // Extending π 100 -> 1000 through one shared handle must equal a from-scratch compute.
548        let h = handle();
549        let _pi_100 = CachedFBig::<mode::HalfAway, 10>::pi(100, &h);
550        let pi_1000 = CachedFBig::<mode::HalfAway, 10>::pi(1000, &h).into_fbig();
551        let direct = Context::<mode::HalfAway>::new(1000).pi::<10>(None).value();
552        assert_eq!(pi_1000, direct);
553    }
554
555    #[test]
556    fn test_cache_survives_arithmetic() {
557        // a and b share one cache handle; the sum must keep it so the subsequent
558        // ln() reuses the same shared cache.
559        let h = handle();
560        let a = CachedFBig::<mode::HalfAway, 10>::from_repr(
561            Repr::new(2.into(), 0),
562            Context::new(30),
563            h.clone(),
564        );
565        let b = CachedFBig::<mode::HalfAway, 10>::from_repr(
566            Repr::new(3.into(), 0),
567            Context::new(30),
568            h.clone(),
569        );
570        let sum_ln = (a.clone() + b.clone()).ln().into_fbig();
571        let expected = (fbig(2, 30) + fbig(3, 30)).ln();
572        assert_eq!(sum_ln, expected);
573    }
574
575    #[test]
576    fn test_arithmetic_matches_fbig() {
577        let a =
578            CachedFBig::<mode::HalfAway, 10>::with_cache(Repr::new(2.into(), 0), Context::new(20));
579        let b =
580            CachedFBig::<mode::HalfAway, 10>::with_cache(Repr::new(3.into(), 0), Context::new(20));
581
582        assert_eq!((a.clone() + b.clone()).into_fbig(), fbig(2, 20) + fbig(3, 20));
583        assert_eq!((a.clone() - b.clone()).into_fbig(), fbig(2, 20) - fbig(3, 20));
584        assert_eq!((a.clone() * b.clone()).into_fbig(), fbig(2, 20) * fbig(3, 20));
585        assert_eq!((a.clone() / b.clone()).into_fbig(), fbig(2, 20) / fbig(3, 20));
586    }
587
588    #[test]
589    fn test_debug_compiles() {
590        let x = CachedFBig::<mode::HalfAway, 10>::with_cache(
591            Repr::new(1234.into(), -3),
592            Context::new(50),
593        );
594        let s = format!("{:?}", x);
595        assert!(s.contains("CachedFBig"));
596    }
597
598    #[test]
599    fn test_arithmetic_with_fbig() {
600        let a =
601            CachedFBig::<mode::HalfAway, 10>::with_cache(Repr::new(2.into(), 0), Context::new(20));
602        let b = fbig(3, 20);
603
604        // CachedFBig op FBig — cache preserved (LHS)
605        let c = a.clone() + b.clone();
606        assert_eq!(c.into_fbig(), fbig(2, 20) + fbig(3, 20));
607
608        // FBig op CachedFBig — cache preserved (RHS)
609        let d = b.clone() + a.clone();
610        assert_eq!(d.into_fbig(), fbig(3, 20) + fbig(2, 20));
611
612        // Sub, Mul, Div
613        assert_eq!((a.clone() - b.clone()).into_fbig(), fbig(2, 20) - fbig(3, 20));
614        assert_eq!((a.clone() * b.clone()).into_fbig(), fbig(2, 20) * fbig(3, 20));
615        assert_eq!((a.clone() / b.clone()).into_fbig(), fbig(2, 20) / fbig(3, 20));
616    }
617
618    #[test]
619    fn test_arithmetic_with_primitives() {
620        let a =
621            CachedFBig::<mode::HalfAway, 10>::with_cache(Repr::new(2.into(), 0), Context::new(20));
622
623        // CachedFBig op primitive
624        assert_eq!((a.clone() + 3u8).into_fbig(), fbig(2, 20) + 3u8);
625        assert_eq!((a.clone() - 3i32).into_fbig(), fbig(2, 20) - 3i32);
626        assert_eq!((a.clone() * 4u64).into_fbig(), fbig(2, 20) * 4u64);
627
628        // Primitive op CachedFBig
629        assert_eq!((3u8 + a.clone()).into_fbig(), 3u8 + fbig(2, 20));
630        assert_eq!((10i32 - a.clone()).into_fbig(), 10i32 - fbig(2, 20));
631    }
632
633    #[test]
634    fn test_cache_size() {
635        let x = CachedFBig::<mode::HalfAway, 10>::with_cache(
636            Repr::new(1234.into(), -3),
637            Context::new(50),
638        );
639        let _ = x.ln();
640        // After computing ln(1.234), the cache should have some state
641        assert!(x.cache().total_terms() > 0);
642        assert!(x.cache().total_words() > 0);
643    }
644
645    #[test]
646    fn test_cache_clear() {
647        let x = CachedFBig::<mode::HalfAway, 10>::with_cache(
648            Repr::new(1234.into(), -3),
649            Context::new(50),
650        );
651        let before_clear = x.ln().into_fbig();
652        assert!(x.cache().total_terms() > 0);
653
654        x.clear_cache();
655        assert_eq!(x.cache().total_terms(), 0);
656        assert_eq!(x.cache().total_words(), 0);
657
658        // After clearing, recomputation still produces the same result
659        let after_clear = x.ln().into_fbig();
660        assert_eq!(after_clear, before_clear);
661    }
662}