use core::fmt;
use dashu_base::{BitTest, EstimatedLog2, Sign, UnsignedAbs};
use dashu_int::{IBig, UBig};
use crate::error::assert_limited_precision;
use crate::fbig::FBig;
use crate::repr::{Context, Repr, Word};
use crate::round::{Round, Rounded};
use crate::utils::ceil_usize;
#[derive(Clone)]
pub(crate) struct CachedState {
pub p: UBig,
pub q: UBig,
pub t: IBig,
pub num_terms: usize,
}
pub struct ConstCache {
pi: Option<CachedState>,
iacoth_6: Option<CachedState>,
iacoth_9: Option<CachedState>,
iacoth_99: Option<CachedState>,
sqrt_10005: Option<UBig>,
sqrt_10005_bits: usize,
}
impl ConstCache {
pub const fn new() -> Self {
Self {
pi: None,
iacoth_6: None,
iacoth_9: None,
iacoth_99: None,
sqrt_10005: None,
sqrt_10005_bits: 0,
}
}
fn sqrt_10005(&mut self, bits: usize) -> (UBig, usize) {
if bits > self.sqrt_10005_bits {
let n = UBig::from(10005u32) << (2 * bits);
self.sqrt_10005 = Some(dashu_base::SquareRoot::sqrt(&n));
self.sqrt_10005_bits = bits;
}
(self.sqrt_10005.as_ref().unwrap().clone(), self.sqrt_10005_bits)
}
#[must_use]
pub fn pi<const B: Word, R: Round>(&mut self, precision: usize) -> Rounded<FBig<R, B>> {
assert_limited_precision(precision);
let bits = bits_for_precision::<B>(precision);
let num_terms = (bits * 100 / 4708) + 1;
let (_p, q, t) = extend_or_compute(&mut self.pi, 0, num_terms, chudnovsky_bs);
let guard_bits = num_terms.bit_len() + 32;
let work_bits = bits + guard_bits;
let work_precision = precision_for_bits::<B>(work_bits);
let work = Context::<R>::new(work_precision);
let (isqrt_val, isqrt_bits) = self.sqrt_10005(work_bits);
let num = IBig::from(426_880) * IBig::from(isqrt_val) * IBig::from(q);
let den = t << isqrt_bits;
let num_f = work.convert_int::<B>(num).value();
let den_f = work.convert_int::<B>(den).value();
let pi = num_f / den_f;
pi.with_precision(precision)
}
fn iacoth<const N: u32, const B: Word, R: Round>(&mut self, precision: usize) -> FBig<R, B> {
let log_b_n = N.log2_est() / B.log2_est();
let required_terms = (precision as f32 / (2.0 * log_b_n)) as usize + 10;
let slot = match N {
6 => &mut self.iacoth_6,
9 => &mut self.iacoth_9,
99 => &mut self.iacoth_99,
_ => unreachable!("iacoth only caches n ∈ {{6, 9, 99}}"),
};
let (_p, q, t) = extend_or_compute(slot, 1, required_terms, |a, b| iacoth_bs(N, a, b));
let guard = ceil_usize(precision.log2_est() / B.log2_est()) + 2;
let work = Context::<R>::new(precision + guard);
let num = work.convert_int::<B>(q.as_ibig() + &t).value();
let denom = work.convert_int::<B>(IBig::from(N) * &q).value();
num / denom
}
#[must_use]
pub fn ln2<const B: Word, R: Round>(&mut self, precision: usize) -> FBig<R, B> {
let work = precision + combine_guard::<B>(precision);
let l6 = self.iacoth::<6, B, R>(work);
let l99 = self.iacoth::<99, B, R>(work);
(4u8 * l6 + 2u8 * l99).with_precision(precision).value()
}
#[must_use]
pub fn ln10<const B: Word, R: Round>(&mut self, precision: usize) -> FBig<R, B> {
let work = precision + combine_guard::<B>(precision);
let l6 = self.iacoth::<6, B, R>(work);
let l99 = self.iacoth::<99, B, R>(work);
let l9 = self.iacoth::<9, B, R>(work);
(12u8 * l6 + 6u8 * l99 + 2u8 * l9)
.with_precision(precision)
.value()
}
#[must_use]
pub fn ln_base<const B: Word, R: Round>(&mut self, precision: usize) -> FBig<R, B> {
match B {
2 => self.ln2::<B, R>(precision),
10 => self.ln10::<B, R>(precision),
b if b.is_power_of_two() => {
let work = precision + combine_guard::<B>(precision);
let bits = b.trailing_zeros() as usize;
(bits * self.ln2::<B, R>(work))
.with_precision(precision)
.value()
}
_ => {
let ctx = Context::<R>::new(precision);
ctx.unwrap_fp(ctx.ln::<B>(
&Repr::new(Repr::<B>::BASE.into(), 0),
None,
))
}
}
}
#[inline]
pub fn total_terms(&self) -> usize {
let sum = |s: &Option<CachedState>| s.as_ref().map_or(0, |s| s.num_terms);
sum(&self.pi) + sum(&self.iacoth_6) + sum(&self.iacoth_9) + sum(&self.iacoth_99)
}
#[inline]
pub fn total_words(&self) -> usize {
let slot_words = |s: &Option<CachedState>| {
s.as_ref().map_or(0, |s| {
s.p.as_words().len() + s.q.as_words().len() + s.t.as_sign_words().1.len()
})
};
slot_words(&self.pi)
+ slot_words(&self.iacoth_6)
+ slot_words(&self.iacoth_9)
+ slot_words(&self.iacoth_99)
+ self.sqrt_10005.as_ref().map_or(0, |s| s.as_words().len())
}
#[inline]
pub fn clear(&mut self) {
self.pi = None;
self.iacoth_6 = None;
self.iacoth_9 = None;
self.iacoth_99 = None;
self.sqrt_10005 = None;
self.sqrt_10005_bits = 0;
}
}
impl Default for ConstCache {
#[inline]
fn default() -> Self {
Self::new()
}
}
fn extend_or_compute<F>(
slot: &mut Option<CachedState>,
start: usize,
target: usize,
range_bs: F,
) -> (UBig, UBig, IBig)
where
F: Fn(usize, usize) -> (UBig, UBig, IBig),
{
match slot {
Some(s) if s.num_terms >= target => (s.p.clone(), s.q.clone(), s.t.clone()),
Some(s) => {
let (pr, qr, tr) = range_bs(s.num_terms, target);
let (p, q, t) = merge(&s.p, &s.q, &s.t, &pr, &qr, &tr);
*slot = Some(CachedState {
p: p.clone(),
q: q.clone(),
t: t.clone(),
num_terms: target,
});
(p, q, t)
}
None => {
let (p, q, t) = range_bs(start, target);
*slot = Some(CachedState {
p: p.clone(),
q: q.clone(),
t: t.clone(),
num_terms: target,
});
(p, q, t)
}
}
}
#[inline]
#[allow(clippy::needless_option_as_deref)]
pub(crate) fn reborrow_cache<'a>(
cache: &'a mut Option<&mut ConstCache>,
) -> Option<&'a mut ConstCache> {
cache.as_deref_mut()
}
fn bits_for_precision<const B: Word>(precision: usize) -> usize {
if B.is_power_of_two() {
precision.saturating_mul(B.ilog2() as usize)
} else {
let ub = B.log2_bounds().1;
ceil_usize(precision as f32 * ub) + 1
}
}
fn precision_for_bits<const B: Word>(bits: usize) -> usize {
if B.is_power_of_two() {
let log2 = B.ilog2() as usize;
(bits + log2 - 1) / log2
} else {
let lb = B.log2_bounds().0;
ceil_usize(bits as f32 / lb) + 1
}
}
fn combine_guard<const B: Word>(precision: usize) -> usize {
ceil_usize(precision.log2_est() / B.log2_est()) + 4
}
pub(crate) fn merge(
pl: &UBig,
ql: &UBig,
tl: &IBig,
pr: &UBig,
qr: &UBig,
tr: &IBig,
) -> (UBig, UBig, IBig) {
let p = pl * pr;
let q = ql * qr;
let t = qr.as_ibig() * tl + pl.as_ibig() * tr;
(p, q, t)
}
pub(crate) fn chudnovsky_bs(a: usize, b: usize) -> (UBig, UBig, IBig) {
if a >= b {
return (UBig::ONE, UBig::ONE, IBig::ZERO);
}
if b - a == 1 {
const COEFF1: IBig = IBig::from_parts_const(Sign::Positive, 13591409);
const COEFF2: IBig = IBig::from_parts_const(Sign::Positive, 545140134);
if a == 0 {
return (UBig::ONE, UBig::ONE, COEFF1);
}
let k = a as u64;
let p = UBig::from(6 * k - 5) * (2 * k - 1) * (6 * k - 1);
let q = UBig::from(k).pow(3) * 10_939_058_860_032_000u64;
let t_val = COEFF1 + COEFF2 * k;
let t_abs = &p * t_val.unsigned_abs();
let t = IBig::from(t_abs) * Sign::from(a % 2 == 1);
return (p, q, t);
}
let mid = (a + b) / 2;
let (p_l, q_l, t_l) = chudnovsky_bs(a, mid);
let (p_r, q_r, t_r) = chudnovsky_bs(mid, b);
merge(&p_l, &q_l, &t_l, &p_r, &q_r, &t_r)
}
pub(crate) fn iacoth_bs(n: u32, a: usize, b: usize) -> (UBig, UBig, IBig) {
debug_assert!(a >= 1, "iacoth_bs leaf index must be >= 1");
if a >= b {
return (UBig::ONE, UBig::ONE, IBig::ZERO); }
if a == 1 {
if let Some((k, p0, q0, t0)) = iacoth_initial_block(n) {
if b > k {
let (pr, qr, tr) = iacoth_bs(n, 1 + k, b);
return merge(&p0, &q0, &t0, &pr, &qr, &tr);
}
}
}
if b - a == 1 {
let pa = UBig::from(2 * a - 1);
let n2 = UBig::from(n).pow(2);
let qa = UBig::from(2 * a + 1) * n2;
let ta = IBig::from(pa.clone());
return (pa, qa, ta);
}
let mid = (a + b) / 2;
let (pl, ql, tl) = iacoth_bs(n, a, mid);
let (pr, qr, tr) = iacoth_bs(n, mid, b);
merge(&pl, &ql, &tl, &pr, &qr, &tr) }
fn iacoth_initial_block(n: u32) -> Option<(usize, UBig, UBig, IBig)> {
match n {
6 => Some(IACOTH_6_INITIAL),
9 => Some(IACOTH_9_INITIAL),
99 => Some(IACOTH_99_INITIAL),
_ => None,
}
}
const IACOTH_6_INITIAL: (usize, UBig, UBig, IBig) = (
4,
UBig::from_word(105),
UBig::from_dword(1587237120),
IBig::from_parts_const(Sign::Positive, 14946549),
);
const IACOTH_9_INITIAL: (usize, UBig, UBig, IBig) = (
3,
UBig::from_word(15),
UBig::from_dword(55801305),
IBig::from_parts_const(Sign::Positive, 231351),
);
const IACOTH_99_INITIAL: (usize, UBig, UBig, IBig) = (
2,
UBig::from_word(3),
UBig::from_dword(1440894015),
IBig::from_parts_const(Sign::Positive, 49008),
);
impl fmt::Debug for ConstCache {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ConstCache")
.field("pi", &DebugSlot(&self.pi))
.field("iacoth_6", &DebugSlot(&self.iacoth_6))
.field("iacoth_9", &DebugSlot(&self.iacoth_9))
.field("iacoth_99", &DebugSlot(&self.iacoth_99))
.finish()
}
}
struct DebugSlot<'a>(&'a Option<CachedState>);
impl fmt::Debug for DebugSlot<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
Some(s) => f
.debug_struct("CachedState")
.field("num_terms", &s.num_terms)
.field("p", &s.p)
.field("q", &s.q)
.field("t", &s.t)
.finish(),
None => f.write_str("None"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::round::mode;
use alloc::format;
#[test]
fn test_iacoth_initial_blocks() {
fn check(n: u32, expected: &(usize, UBig, UBig, IBig)) {
let k = expected.0;
let mut acc = (UBig::ONE, UBig::ONE, IBig::ZERO);
for kk in 1..=k {
let pa = UBig::from(2 * kk as u64 - 1);
let qa = UBig::from(2 * kk as u64 + 1) * UBig::from(n).pow(2);
let ta = IBig::from(pa.clone());
let (p, q, t) = merge(&acc.0, &acc.1, &acc.2, &pa, &qa, &ta);
acc = (p, q, t);
}
assert_eq!(acc.0, expected.1, "P mismatch for n={n}");
assert_eq!(acc.1, expected.2, "Q mismatch for n={n}");
assert_eq!(acc.2, expected.3, "T mismatch for n={n}");
assert_eq!(iacoth_bs(n, 1, 1 + k), (acc.0, acc.1, acc.2));
}
check(6, &IACOTH_6_INITIAL);
check(9, &IACOTH_9_INITIAL);
check(99, &IACOTH_99_INITIAL);
}
#[test]
fn test_pi_matches_context() {
for &precision in &[10usize, 50, 100] {
let mut cache = ConstCache::new();
let cached = cache.pi::<10, mode::HalfEven>(precision).value();
let direct = Context::<mode::HalfEven>::new(precision)
.pi::<10>(None)
.value();
assert_eq!(cached, direct, "pi mismatch at precision {precision}");
}
}
#[test]
fn test_pi_lower_precision_reuses() {
let mut cache = ConstCache::new();
let _pi_high = cache.pi::<10, mode::HalfEven>(200).value();
let pi_50 = cache.pi::<10, mode::HalfEven>(50).value();
let direct = Context::<mode::HalfEven>::new(50).pi::<10>(None).value();
assert_eq!(pi_50, direct);
}
#[test]
fn test_pi_extension_matches_scratch() {
let mut cache = ConstCache::new();
let _pi_100 = cache.pi::<10, mode::HalfAway>(100).value();
let pi_1000_extended = cache.pi::<10, mode::HalfAway>(1000).value();
let direct = Context::<mode::HalfAway>::new(1000).pi::<10>(None).value();
assert_eq!(pi_1000_extended, direct);
}
#[test]
fn test_iacoth_matches_context() {
let mut cache = ConstCache::new();
for &precision in &[20usize, 45, 80] {
let cached_ln2 = cache
.ln2::<10, mode::Zero>(precision)
.with_precision(precision)
.value();
let ln2_ctx = Context::<mode::Zero>::new(precision);
let direct_ln2 = ln2_ctx.unwrap_fp(ln2_ctx.ln::<10>(&Repr::new(2.into(), 0), None));
assert_eq!(cached_ln2, direct_ln2, "ln2 mismatch at precision {precision}");
let cached_ln10 = cache
.ln10::<10, mode::Zero>(precision)
.with_precision(precision)
.value();
let ln10_ctx = Context::<mode::Zero>::new(precision);
let direct_ln10 = ln10_ctx.unwrap_fp(ln10_ctx.ln::<10>(&Repr::new(10.into(), 0), None));
assert_eq!(cached_ln10, direct_ln10, "ln10 mismatch at precision {precision}");
}
}
#[test]
fn test_iacoth_extension_matches_scratch() {
let mut cache = ConstCache::new();
let _ln2_low = cache.ln2::<10, mode::HalfAway>(20);
let ln2_high = cache.ln2::<10, mode::HalfAway>(120);
let mut fresh = ConstCache::new();
let direct = fresh.ln2::<10, mode::HalfAway>(120);
assert_eq!(ln2_high, direct);
}
#[test]
fn test_ln_base() {
let mut cache = ConstCache::new();
let ln_base = cache.ln_base::<2, mode::HalfAway>(50);
let ln2 = cache.ln2::<2, mode::HalfAway>(50);
assert_eq!(ln_base, ln2);
let ln8 = cache.ln_base::<8, mode::HalfAway>(50);
let expected = 3u8 * cache.ln2::<8, mode::HalfAway>(50);
assert_eq!(ln8.with_precision(50).value(), expected.with_precision(50).value());
}
#[test]
fn test_debug_shows_bigint_head_tail() {
let mut cache = ConstCache::new();
let _pi = cache.pi::<10, mode::HalfAway>(100); let s = format!("{:?}", cache);
assert!(s.contains("pi"));
assert!(s.contains("num_terms"));
assert!(s.contains(".."), "Debug output should use head..tail truncation");
assert!(s.len() < 512);
}
#[test]
fn test_sqrt_10005_cached_and_counted() {
let mut cache = ConstCache::new();
assert_eq!(cache.total_terms(), 0);
assert_eq!(cache.total_words(), 0);
let _pi = cache.pi::<10, mode::HalfAway>(200); assert!(cache.total_words() > 0);
cache.clear();
assert_eq!(cache.total_terms(), 0);
assert_eq!(cache.total_words(), 0);
let direct = Context::<mode::HalfAway>::new(50).pi::<10>(None).value();
let after_clear = cache.pi::<10, mode::HalfAway>(50).value();
assert_eq!(after_clear, direct);
}
#[test]
fn test_sqrt_10005_reuse_higher_precision() {
let mut cache = ConstCache::new();
let _high = cache.pi::<2, mode::HalfEven>(1000);
let words_after_high = cache.total_words();
let low = cache.pi::<2, mode::HalfEven>(100).value();
assert_eq!(cache.total_words(), words_after_high);
let direct = Context::<mode::HalfEven>::new(100).pi::<2>(None).value();
assert_eq!(low, direct);
}
}