use core::cmp::Ordering;
use dashu_base::Approximation::*;
use crate::{
error::{FpError, FpResult},
fbig::FBig,
repr::{Context, Repr},
round::ErrorBounds,
};
use dashu_int::Word;
const MAX_ZIV_RETRIES: usize = 32;
#[cfg(any(all(test, feature = "std"), feature = "tuning"))]
thread_local! {
pub(crate) static LAST_ZIV_RETRIES: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
}
#[cfg(any(all(test, feature = "std"), feature = "tuning"))]
fn ziv_retries_reset_impl() {
LAST_ZIV_RETRIES.with(|c| c.set(0));
}
#[cfg(not(any(all(test, feature = "std"), feature = "tuning")))]
fn ziv_retries_reset_impl() {}
#[cfg(any(all(test, feature = "std"), feature = "tuning"))]
fn ziv_retries_bump() {
LAST_ZIV_RETRIES.with(|c| c.set(c.get().saturating_add(1)));
}
#[cfg(not(any(all(test, feature = "std"), feature = "tuning")))]
fn ziv_retries_bump() {}
#[cfg(any(all(test, feature = "std"), feature = "tuning"))]
pub fn ziv_retries() -> usize {
LAST_ZIV_RETRIES.with(|c| c.get())
}
#[cfg(any(all(test, feature = "std"), feature = "tuning"))]
pub fn ziv_retries_reset() {
ziv_retries_reset_impl();
}
impl<R: ErrorBounds> Context<R> {
pub(crate) fn ziv<const B: Word>(
&self,
initial_guard: usize,
mut approx: impl FnMut(usize) -> Result<(FBig<R, B>, FBig<R, B>), FpError>,
) -> FpResult<FBig<R, B>> {
if !self.is_limited() {
let (value, _err) = approx(0)?;
return Ok(Exact(value));
}
let mut guard = initial_guard;
ziv_retries_reset_impl();
for _ in 0..MAX_ZIV_RETRIES {
let (a, e) = approx(guard)?;
let candidate = a.clone().with_precision(self.precision);
if Self::contained::<B>(&a.repr, &e.repr, candidate.value_ref()) {
return Ok(candidate);
}
let step = core::cmp::max(guard, self.precision / 2).max(1);
guard = guard.saturating_add(step).min(usize::MAX - self.precision);
ziv_retries_bump();
}
Err(FpError::ZivRetryLimitExceeded)
}
pub(crate) fn ziv_pair<const B: Word>(
&self,
initial_guard: usize,
mut approx: impl FnMut(
usize,
)
-> Result<((FBig<R, B>, FBig<R, B>), (FBig<R, B>, FBig<R, B>)), FpError>,
) -> (FpResult<FBig<R, B>>, FpResult<FBig<R, B>>) {
if !self.is_limited() {
let ((v1, _), (v2, _)) = match approx(0) {
Ok(v) => v,
Err(e) => return (Err(e), Err(e)),
};
return (Ok(Exact(v1)), Ok(Exact(v2)));
}
let mut guard = initial_guard;
ziv_retries_reset_impl();
for _ in 0..MAX_ZIV_RETRIES {
let ((a1, e1), (a2, e2)) = match approx(guard) {
Ok(v) => v,
Err(e) => return (Err(e), Err(e)),
};
let c1 = a1.clone().with_precision(self.precision);
let c2 = a2.clone().with_precision(self.precision);
if Self::contained::<B>(&a1.repr, &e1.repr, c1.value_ref())
&& Self::contained::<B>(&a2.repr, &e2.repr, c2.value_ref())
{
return (Ok(c1), Ok(c2));
}
let step = core::cmp::max(guard, self.precision / 2).max(1);
guard = guard.saturating_add(step).min(usize::MAX - self.precision);
ziv_retries_bump();
}
(Err(FpError::ZivRetryLimitExceeded), Err(FpError::ZivRetryLimitExceeded))
}
fn contained<const B: Word>(a: &Repr<B>, e: &Repr<B>, y: &FBig<R, B>) -> bool {
let (lb, rb, incl_l, incl_r) = R::error_bounds::<B>(y);
let y = &y.repr;
let lb = lb.into_repr();
let rb = rb.into_repr();
let left = (a + &lb).cmp(&(y + e));
let right = (y + &rb).cmp(&(a + e));
let left_ok = if incl_l {
left != Ordering::Less
} else {
left == Ordering::Greater
};
let right_ok = if incl_r {
right != Ordering::Less
} else {
right == Ordering::Greater
};
left_ok && right_ok
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use crate::round::mode;
type F = crate::FBig<mode::HalfEven>;
#[test]
fn ziv_accepts_exact_first_attempt() {
let ctx: Context<mode::HalfEven> = Context::new(10);
LAST_ZIV_RETRIES.with(|c| c.set(usize::MAX));
let r = ctx.ziv(4, |_| Ok((F::ONE, F::ZERO)));
assert!(matches!(r, Ok(Exact(_))));
assert_eq!(LAST_ZIV_RETRIES.with(|c| c.get()), 0);
}
#[test]
fn ziv_retries_until_contained() {
let ctx: Context<mode::HalfEven> = Context::new(4);
let r = ctx.ziv(2, |guard| {
Ok((F::ONE, F::ONE >> guard as isize))
});
let _ = r.unwrap().value();
assert!(LAST_ZIV_RETRIES.with(|c| c.get()) >= 1);
}
#[test]
fn ziv_unlimited_short_circuits() {
let ctx: Context<mode::HalfEven> = Context::new(0);
LAST_ZIV_RETRIES.with(|c| c.set(usize::MAX));
let r = ctx.ziv(4, |_| Ok((F::from(7u8), F::ZERO)));
assert!(matches!(r, Ok(Exact(_))));
assert_eq!(LAST_ZIV_RETRIES.with(|c| c.get()), usize::MAX);
}
#[test]
fn ziv_propagates_closure_error() {
let ctx: Context<mode::HalfEven> = Context::new(10);
let r = ctx.ziv::<2>(4, |_| Err(FpError::OutOfDomain));
assert_eq!(r, Err(FpError::OutOfDomain));
}
#[test]
fn ziv_reports_retry_limit_exceeded() {
let ctx: Context<mode::HalfEven> = Context::new(4);
let r = ctx.ziv(2, |_| Ok((F::ONE, F::from(10u8))));
assert_eq!(r, Err(FpError::ZivRetryLimitExceeded));
let (r1, r2) = ctx.ziv_pair(2, |_| Ok(((F::ONE, F::from(10u8)), (F::ONE, F::ZERO))));
assert_eq!(r1, Err(FpError::ZivRetryLimitExceeded));
assert_eq!(r2, Err(FpError::ZivRetryLimitExceeded));
}
#[test]
fn ziv_pair_accepts_exact_first_attempt() {
let ctx: Context<mode::HalfEven> = Context::new(10);
LAST_ZIV_RETRIES.with(|c| c.set(usize::MAX));
let (r1, r2) = ctx.ziv_pair(4, |_| Ok(((F::ONE, F::ZERO), (F::from(2u8), F::ZERO))));
assert!(matches!(r1, Ok(Exact(_))));
assert!(matches!(r2, Ok(Exact(_))));
assert_eq!(LAST_ZIV_RETRIES.with(|c| c.get()), 0);
}
#[test]
fn ziv_pair_retries_until_both_contained() {
let ctx: Context<mode::HalfEven> = Context::new(4);
let (r1, r2) = ctx.ziv_pair(2, |guard| {
let radius = F::ONE >> guard as isize;
Ok(((F::ONE, F::ZERO), (F::ONE, radius)))
});
let _ = (r1.unwrap().value(), r2.unwrap().value());
assert!(LAST_ZIV_RETRIES.with(|c| c.get()) >= 1);
}
#[test]
fn ziv_pair_propagates_closure_error() {
let ctx: Context<mode::HalfEven> = Context::new(10);
let (r1, r2) = ctx.ziv_pair::<2>(4, |_| Err(FpError::OutOfDomain));
assert_eq!(r1, Err(FpError::OutOfDomain));
assert_eq!(r2, Err(FpError::OutOfDomain));
}
#[test]
fn ziv_few_retries_for_typical_inputs() {
let cases = [
F::try_from(0.5f64).unwrap(),
F::try_from(1.5f64).unwrap(),
F::try_from(2.0f64).unwrap(),
F::try_from(10.0f64).unwrap(),
F::try_from(1000.0f64).unwrap(),
F::try_from(1e-6f64).unwrap(),
];
const MAX_RETRIES: usize = 1;
for p in [10usize, 24, 53, 100, 200] {
for x in &cases {
let x = x.clone().with_precision(p).value();
LAST_ZIV_RETRIES.with(|c| c.set(usize::MAX));
let _ = x.ln();
let ln_retries = LAST_ZIV_RETRIES.with(|c| c.get());
assert!(
ln_retries <= MAX_RETRIES,
"ln({x}) at p={p} took {ln_retries} retries (expected <= {MAX_RETRIES})"
);
LAST_ZIV_RETRIES.with(|c| c.set(usize::MAX));
let _ = x.exp();
let exp_retries = LAST_ZIV_RETRIES.with(|c| c.get());
assert!(
exp_retries <= MAX_RETRIES,
"exp({x}) at p={p} took {exp_retries} retries (expected <= {MAX_RETRIES})"
);
}
}
}
}