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`]/[`UpperHex`](core::fmt::UpperHex)) onto [`CachedFBig`] for the bases where they
415/// apply, delegating each to the inner [`FBig`]'s impl so the output matches.
416macro_rules! impl_cached_fmt_with_base {
417    ($base:literal, $trait:ident) => {
418        impl<R: Round> core::fmt::$trait for CachedFBig<R, $base> {
419            #[inline]
420            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
421                core::fmt::$trait::fmt(&self.fbig, f)
422            }
423        }
424    };
425}
426impl_cached_fmt_with_base!(2, Binary);
427impl_cached_fmt_with_base!(2, LowerHex);
428impl_cached_fmt_with_base!(2, UpperHex);
429impl_cached_fmt_with_base!(8, Octal);
430impl_cached_fmt_with_base!(16, LowerHex);
431impl_cached_fmt_with_base!(16, UpperHex);
432
433impl<R1: Round, R2: Round, const B: Word> PartialEq<CachedFBig<R2, B>> for CachedFBig<R1, B> {
434    #[inline]
435    fn eq(&self, other: &CachedFBig<R2, B>) -> bool {
436        // value equality, mirroring FBig (compares the representation only).
437        self.fbig.repr == other.fbig.repr
438    }
439}
440
441impl<R: Round, const B: Word> Eq for CachedFBig<R, B> {}
442
443// ---------------------------------------------------------------------------
444// Ordering and the dashu-base ordering/log/sign traits
445// (delegate to the inner FBig — value ordering, context ignored)
446// ---------------------------------------------------------------------------
447
448impl<R1: Round, R2: Round, const B: Word> PartialOrd<CachedFBig<R2, B>> for CachedFBig<R1, B> {
449    #[inline]
450    fn partial_cmp(&self, other: &CachedFBig<R2, B>) -> Option<Ordering> {
451        self.fbig.partial_cmp(&other.fbig)
452    }
453}
454
455impl<R: Round, const B: Word> Ord for CachedFBig<R, B> {
456    #[inline]
457    fn cmp(&self, other: &Self) -> Ordering {
458        self.fbig.cmp(&other.fbig)
459    }
460}
461
462impl<R: Round, const B: Word> AbsOrd for CachedFBig<R, B> {
463    #[inline]
464    fn abs_cmp(&self, other: &Self) -> Ordering {
465        AbsOrd::abs_cmp(&self.fbig, &other.fbig)
466    }
467}
468
469impl<R: Round, const B: Word> AbsOrd<UBig> for CachedFBig<R, B> {
470    #[inline]
471    fn abs_cmp(&self, other: &UBig) -> Ordering {
472        AbsOrd::abs_cmp(&self.fbig, other)
473    }
474}
475impl<R: Round, const B: Word> AbsOrd<CachedFBig<R, B>> for UBig {
476    #[inline]
477    fn abs_cmp(&self, other: &CachedFBig<R, B>) -> Ordering {
478        AbsOrd::abs_cmp(self, &other.fbig)
479    }
480}
481impl<R: Round, const B: Word> AbsOrd<IBig> for CachedFBig<R, B> {
482    #[inline]
483    fn abs_cmp(&self, other: &IBig) -> Ordering {
484        AbsOrd::abs_cmp(&self.fbig, other)
485    }
486}
487impl<R: Round, const B: Word> AbsOrd<CachedFBig<R, B>> for IBig {
488    #[inline]
489    fn abs_cmp(&self, other: &CachedFBig<R, B>) -> Ordering {
490        AbsOrd::abs_cmp(self, &other.fbig)
491    }
492}
493
494impl<R: Round, const B: Word> Signed for CachedFBig<R, B> {
495    #[inline]
496    fn sign(&self) -> Sign {
497        self.fbig.sign()
498    }
499}
500
501impl<R: Round, const B: Word> EstimatedLog2 for CachedFBig<R, B> {
502    #[inline]
503    fn log2_bounds(&self) -> (f32, f32) {
504        EstimatedLog2::log2_bounds(&self.fbig)
505    }
506
507    #[inline]
508    fn log2_est(&self) -> f32 {
509        EstimatedLog2::log2_est(&self.fbig)
510    }
511}
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516    use crate::round::mode;
517    use alloc::format;
518
519    fn handle() -> Rc<RefCell<ConstCache>> {
520        Rc::new(RefCell::new(ConstCache::new()))
521    }
522
523    /// An `FBig` with value `n` at the given precision (so inexact results match the
524    /// `CachedFBig` operands built at the same precision).
525    fn fbig(n: i32, prec: usize) -> FBig<mode::HalfAway, 10> {
526        FBig::from_repr(Repr::new(n.into(), 0), Context::new(prec))
527    }
528
529    #[test]
530    fn test_pi_matches_fbig() {
531        for &precision in &[10usize, 50, 100] {
532            let h = handle();
533            let cached = CachedFBig::<mode::HalfAway, 10>::pi(precision, &h).into_fbig();
534            let direct = FBig::<mode::HalfAway, 10>::pi(precision);
535            assert_eq!(cached, direct, "pi mismatch at precision {precision}");
536        }
537    }
538
539    #[test]
540    fn test_transcendentals_match_fbig() {
541        let x = CachedFBig::<mode::HalfAway, 10>::with_cache(
542            Repr::new(1234.into(), -3), // 1.234
543            Context::new(50),
544        );
545        let y = FBig::<mode::HalfAway, 10>::from_repr(Repr::new(1234.into(), -3), Context::new(50));
546
547        assert_eq!(x.clone().ln().into_fbig(), y.clone().ln());
548        assert_eq!(x.clone().exp().into_fbig(), y.clone().exp());
549        assert_eq!(x.clone().sin().into_fbig(), y.clone().sin());
550        assert_eq!(x.clone().cos().into_fbig(), y.clone().cos());
551        assert_eq!(x.clone().exp_m1().into_fbig(), y.clone().exp_m1());
552        assert_eq!(x.clone().ln_1p().into_fbig(), y.clone().ln_1p());
553        assert_eq!(x.clone().log2().into_fbig(), y.clone().log2());
554        assert_eq!(x.powf(&x.clone()).into_fbig(), y.clone().powf(&y));
555    }
556
557    #[test]
558    fn test_cache_extension_matches_scratch() {
559        // Extending π 100 -> 1000 through one shared handle must equal a from-scratch compute.
560        let h = handle();
561        let _pi_100 = CachedFBig::<mode::HalfAway, 10>::pi(100, &h);
562        let pi_1000 = CachedFBig::<mode::HalfAway, 10>::pi(1000, &h).into_fbig();
563        let direct = Context::<mode::HalfAway>::new(1000).pi::<10>(None).value();
564        assert_eq!(pi_1000, direct);
565    }
566
567    #[test]
568    fn test_cache_survives_arithmetic() {
569        // a and b share one cache handle; the sum must keep it so the subsequent
570        // ln() reuses the same shared cache.
571        let h = handle();
572        let a = CachedFBig::<mode::HalfAway, 10>::from_repr(
573            Repr::new(2.into(), 0),
574            Context::new(30),
575            h.clone(),
576        );
577        let b = CachedFBig::<mode::HalfAway, 10>::from_repr(
578            Repr::new(3.into(), 0),
579            Context::new(30),
580            h.clone(),
581        );
582        let sum_ln = (a.clone() + b.clone()).ln().into_fbig();
583        let expected = (fbig(2, 30) + fbig(3, 30)).ln();
584        assert_eq!(sum_ln, expected);
585    }
586
587    #[test]
588    fn test_arithmetic_matches_fbig() {
589        let a =
590            CachedFBig::<mode::HalfAway, 10>::with_cache(Repr::new(2.into(), 0), Context::new(20));
591        let b =
592            CachedFBig::<mode::HalfAway, 10>::with_cache(Repr::new(3.into(), 0), Context::new(20));
593
594        assert_eq!((a.clone() + b.clone()).into_fbig(), fbig(2, 20) + fbig(3, 20));
595        assert_eq!((a.clone() - b.clone()).into_fbig(), fbig(2, 20) - fbig(3, 20));
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    }
599
600    #[test]
601    fn test_debug_compiles() {
602        let x = CachedFBig::<mode::HalfAway, 10>::with_cache(
603            Repr::new(1234.into(), -3),
604            Context::new(50),
605        );
606        let s = format!("{:?}", x);
607        assert!(s.contains("CachedFBig"));
608    }
609
610    #[test]
611    fn test_arithmetic_with_fbig() {
612        let a =
613            CachedFBig::<mode::HalfAway, 10>::with_cache(Repr::new(2.into(), 0), Context::new(20));
614        let b = fbig(3, 20);
615
616        // CachedFBig op FBig — cache preserved (LHS)
617        let c = a.clone() + b.clone();
618        assert_eq!(c.into_fbig(), fbig(2, 20) + fbig(3, 20));
619
620        // FBig op CachedFBig — cache preserved (RHS)
621        let d = b.clone() + a.clone();
622        assert_eq!(d.into_fbig(), fbig(3, 20) + fbig(2, 20));
623
624        // Sub, Mul, Div
625        assert_eq!((a.clone() - b.clone()).into_fbig(), fbig(2, 20) - fbig(3, 20));
626        assert_eq!((a.clone() * b.clone()).into_fbig(), fbig(2, 20) * fbig(3, 20));
627        assert_eq!((a.clone() / b.clone()).into_fbig(), fbig(2, 20) / fbig(3, 20));
628    }
629
630    #[test]
631    fn test_arithmetic_with_primitives() {
632        let a =
633            CachedFBig::<mode::HalfAway, 10>::with_cache(Repr::new(2.into(), 0), Context::new(20));
634
635        // CachedFBig op primitive
636        assert_eq!((a.clone() + 3u8).into_fbig(), fbig(2, 20) + 3u8);
637        assert_eq!((a.clone() - 3i32).into_fbig(), fbig(2, 20) - 3i32);
638        assert_eq!((a.clone() * 4u64).into_fbig(), fbig(2, 20) * 4u64);
639
640        // Primitive op CachedFBig
641        assert_eq!((3u8 + a.clone()).into_fbig(), 3u8 + fbig(2, 20));
642        assert_eq!((10i32 - a.clone()).into_fbig(), 10i32 - fbig(2, 20));
643    }
644
645    #[test]
646    fn test_cache_size() {
647        let x = CachedFBig::<mode::HalfAway, 10>::with_cache(
648            Repr::new(1234.into(), -3),
649            Context::new(50),
650        );
651        let _ = x.ln();
652        // After computing ln(1.234), the cache should have some state
653        assert!(x.cache().total_terms() > 0);
654        assert!(x.cache().total_words() > 0);
655    }
656
657    #[test]
658    fn test_cache_clear() {
659        let x = CachedFBig::<mode::HalfAway, 10>::with_cache(
660            Repr::new(1234.into(), -3),
661            Context::new(50),
662        );
663        let before_clear = x.ln().into_fbig();
664        assert!(x.cache().total_terms() > 0);
665
666        x.clear_cache();
667        assert_eq!(x.cache().total_terms(), 0);
668        assert_eq!(x.cache().total_words(), 0);
669
670        // After clearing, recomputation still produces the same result
671        let after_clear = x.ln().into_fbig();
672        assert_eq!(after_clear, before_clear);
673    }
674}