Skip to main content

dashu_float/
log.rs

1use dashu_base::{
2    utils::{next_down, next_up},
3    Approximation::*,
4    BitTest, EstimatedLog2, PowerOfTwo, Sign, UnsignedAbs,
5};
6use dashu_int::{IBig, UBig};
7
8use crate::{
9    ball::{ulps, Ball},
10    cmp::repr_cmp_same_base,
11    error::{assert_finite, assert_limited_precision, FpError, FpResult},
12    fbig::FBig,
13    mag::Mag,
14    math::cache::{reborrow_cache, ConstCache},
15    repr::{Context, Repr, Word},
16    round::{mode, ErrorBounds, Round},
17};
18use core::cmp::Ordering;
19
20/// Extra digits for the reconstruction constants (see `ln_compute`): a scale factor `|k|`
21/// amplifies a constant's 8-ulp radius by `|k|`; `extra > log_B(|k|)` absorbs the `|k|`
22/// amplification, leaving the scaled radius at ≤ 8 work-ulps (a constant fraction of the
23/// result's ulp, sub-target once the Ziv guard is added; for the smallest bases it is a few
24/// work-ulps, absorbed by the loop's initial guard). Computed from the factor's bit length,
25/// integer-only: `log_B(|k|) < bit_len(|k|) / ilog2(B)`.
26fn const_extra_digits<const B: Word>(mag_bit_len: usize) -> usize {
27    mag_bit_len / (Word::BITS - 1 - B.leading_zeros()) as usize + 1
28}
29
30impl<const B: Word> EstimatedLog2 for Repr<B> {
31    // currently a Word has at most 64 bits, so log2() < f32::MAX
32    fn log2_bounds(&self) -> (f32, f32) {
33        if self.significand.is_zero() {
34            return (f32::NEG_INFINITY, f32::NEG_INFINITY);
35        }
36
37        // log(s*B^e) = log(s) + e*log(B)
38        let (logs_lb, logs_ub) = self.significand.log2_bounds();
39        let (logb_lb, logb_ub) = if B.is_power_of_two() {
40            let log = B.trailing_zeros() as f32;
41            (log, log)
42        } else {
43            B.log2_bounds()
44        };
45        let e = self.exponent as f32;
46        let (lb, ub) = if self.exponent >= 0 {
47            (logs_lb + e * logb_lb, logs_ub + e * logb_ub)
48        } else {
49            (logs_lb + e * logb_ub, logs_ub + e * logb_lb)
50        };
51        (next_down(lb), next_up(ub))
52    }
53
54    fn log2_est(&self) -> f32 {
55        let logs = self.significand.log2_est();
56        let logb = if B.is_power_of_two() {
57            B.trailing_zeros() as f32
58        } else {
59            B.log2_est()
60        };
61        logs + self.exponent as f32 * logb
62    }
63}
64
65impl<R: Round, const B: Word> EstimatedLog2 for FBig<R, B> {
66    #[inline]
67    fn log2_bounds(&self) -> (f32, f32) {
68        self.repr.log2_bounds()
69    }
70
71    #[inline]
72    fn log2_est(&self) -> f32 {
73        self.repr.log2_est()
74    }
75}
76
77impl<R: ErrorBounds, const B: Word> FBig<R, B> {
78    /// Calculate the natural logarithm function (`log(x)`) on the float number.
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// # use core::str::FromStr;
84    /// # use dashu_base::ParseError;
85    /// # use dashu_float::DBig;
86    /// let a = DBig::from_str("1.234")?;
87    /// assert_eq!(a.ln(), DBig::from_str("0.2103")?);
88    /// # Ok::<(), ParseError>(())
89    /// ```
90    #[inline]
91    pub fn ln(&self) -> Self {
92        self.context.unwrap_fp(self.context.ln(&self.repr, None))
93    }
94
95    /// Calculate the natural logarithm function (`log(x+1)`) on the float number
96    ///
97    /// # Examples
98    ///
99    /// ```
100    /// # use core::str::FromStr;
101    /// # use dashu_base::ParseError;
102    /// # use dashu_float::DBig;
103    /// let a = DBig::from_str("0.1234")?;
104    /// assert_eq!(a.ln_1p(), DBig::from_str("0.11636")?);
105    /// # Ok::<(), ParseError>(())
106    /// ```
107    #[inline]
108    pub fn ln_1p(&self) -> Self {
109        self.context.unwrap_fp(self.context.ln_1p(&self.repr, None))
110    }
111
112    /// Calculate the base-2 logarithm (`log2(x)`) on the float number.
113    ///
114    /// Correctly rounded to the context's precision under any rounding mode. For an exact power
115    /// of two the result is the exact integer `log2(x)`.
116    ///
117    /// # Examples
118    ///
119    /// ```
120    /// # use core::str::FromStr;
121    /// # use dashu_base::ParseError;
122    /// # use dashu_float::DBig;
123    /// let a = DBig::from_str("8")?;
124    /// assert_eq!(a.log2(), DBig::from_str("3")?);
125    /// # Ok::<(), ParseError>(())
126    /// ```
127    #[inline]
128    pub fn log2(&self) -> Self {
129        self.context.unwrap_fp(self.context.log2(&self.repr, None))
130    }
131
132    /// Calculate the base-10 logarithm (`log10(x)`) on the float number.
133    ///
134    /// Correctly rounded to the context's precision under any rounding mode. For an exact power
135    /// of ten the result is the exact integer `log10(x)`.
136    ///
137    /// # Examples
138    ///
139    /// ```
140    /// # use core::str::FromStr;
141    /// # use dashu_base::ParseError;
142    /// # use dashu_float::DBig;
143    /// let a = DBig::from_str("1000")?;
144    /// assert_eq!(a.log10(), DBig::from_str("3")?);
145    /// # Ok::<(), ParseError>(())
146    /// ```
147    #[inline]
148    pub fn log10(&self) -> Self {
149        self.context.unwrap_fp(self.context.log10(&self.repr, None))
150    }
151}
152
153// `ln2`/`ln10`/`iacoth`/`ln_base`/`ln_compute` are the near-correct logarithm primitives: they
154// evaluate the series at a working precision and round once, without a Ziv certification step.
155// They live on `R: Round` so that base conversion (`with_base_and_precision`, which only needs a
156// near-correct constant `ln(B)`) can use them without inheriting the `ErrorBounds` bound. The
157// correctly-rounded public `ln`/`ln_1p` (in the `ErrorBounds` impl below) wrap `ln_compute` in a
158// Ziv loop.
159impl<R: Round> Context<R> {
160    /// Calculate log(2)
161    ///
162    /// The precision of the output will be larger than self.precision
163    #[inline]
164    fn ln2<const B: Word>(&self, cache: Option<&mut ConstCache>) -> FBig<R, B> {
165        if let Some(c) = cache {
166            return c.ln2::<B, R>(self.precision);
167        }
168        // log(2) = 4L(6) + 2L(99)
169        // see formula (24) from Gourdon, Xavier, and Pascal Sebah.
170        // "The Logarithmic Constant: Log 2." (2004)
171        4 * self.iacoth(6.into()) + 2 * self.iacoth(99.into())
172    }
173
174    /// Calculate log(10)
175    ///
176    /// The precision of the output will be larger than self.precision
177    #[inline]
178    fn ln10<const B: Word>(&self, cache: Option<&mut ConstCache>) -> FBig<R, B> {
179        if let Some(c) = cache {
180            return c.ln10::<B, R>(self.precision);
181        }
182        // log(10) = log(2) + log(5) = 3log(2) + 2L(9)
183        3 * self.ln2(None) + 2 * self.iacoth(9.into())
184    }
185
186    /// Calculate log(B), for internal use only
187    ///
188    /// The precision of the output will be larger than self.precision
189    #[inline]
190    pub(crate) fn ln_base<const B: Word>(&self, cache: Option<&mut ConstCache>) -> FBig<R, B> {
191        if let Some(c) = cache {
192            return c.ln_base::<B, R>(self.precision);
193        }
194        match B {
195            2 => self.ln2(None),
196            10 => self.ln10(None),
197            i if i.is_power_of_two() => self.ln2(None) * i.trailing_zeros(),
198            _ => {
199                // Near-correct ln(B) via the atanh series (no Ziv certification — base conversion
200                // only needs a near-correct constant). `ln_compute` is on `R: Round`, so this keeps
201                // `ln_base` callable from `R: Round` contexts (base conversion).
202                let guard = self.base_guard_digits::<B>() + 2;
203                self.ln_compute::<B>(
204                    &Repr::new(Repr::<B>::BASE.into(), 0),
205                    self.precision + guard,
206                    false,
207                    None,
208                )
209                .expect("ln(BASE) cannot range-error (the base fits the exponent range)")
210                .to_value_radius::<R>(&Context::<R>::new(self.precision + guard))
211                .0
212            }
213        }
214    }
215
216    /// `ln(B)` as a [`Ball`], carrying the mechanical radius.
217    ///
218    /// The cached bases (2, 10, powers of 2) evaluate correctly-rounded constants (error ≤ ½ ulp at
219    /// the working precision), so the fixed `8` ulps is a sound loose bound. A generic base falls
220    /// back to `ln_compute`'s atanh series, whose radius is ~(series terms + B) ulps — far larger
221    /// than 8 — so its mechanical radius is kept instead (a hard-coded `8` would under-bind the
222    /// `s·ln(B)` reconstruction error in `exp_compute`'s reduction).
223    pub(crate) fn ln_base_ball<const B: Word>(
224        &self,
225        mut cache: Option<&mut ConstCache>,
226    ) -> Result<Ball<B>, FpError> {
227        let ctx = Context::<mode::HalfEven>::new(self.precision);
228        match B {
229            10 => {
230                let logb = ctx.ln_base::<B>(reborrow_cache(&mut cache));
231                let rad = ulps::<B>(&logb.repr, self.precision, 8);
232                Ok(Ball::with_error(logb.into_repr(), rad))
233            }
234            i if i.is_power_of_two() => {
235                let logb = ctx.ln_base::<B>(reborrow_cache(&mut cache));
236                let rad = ulps::<B>(&logb.repr, self.precision, 8);
237                Ok(Ball::with_error(logb.into_repr(), rad))
238            }
239            _ => {
240                // Generic base: no cached sub-series applies, so compute ln(B) directly at the
241                // requested precision and keep `ln_compute`'s ball error.
242                ctx.ln_compute::<B>(
243                    &Repr::new(Repr::<B>::BASE.into(), 0),
244                    self.precision,
245                    false,
246                    reborrow_cache(&mut cache),
247                )
248            }
249        }
250    }
251
252    /// Calculate L(n) = acoth(n) = atanh(1/n) = 1/2 log((n+1)/(n-1)), given by the
253    /// series
254    ///
255    /// ```text
256    ///                1     n + 1              1
257    ///   atanh(1/n) = — log(—————) = Σ   ——————————————————
258    ///                2     n - 1   i≥0 n^(2i+1) · (2i+1)
259    /// ```
260    ///
261    /// This method is intended to be used in logarithm calculation,
262    /// so the precision of the output will be larger than desired precision.
263    ///
264    /// Evaluated by binary splitting (see [`iacoth_bs`][crate::math::cache::iacoth_bs]):
265    /// the exact integer tree state `(P, Q, T)` over `[1, N)` satisfies
266    /// `L(n) = (Q + T)/(n·Q)`, with `Q` kept at O(p) digits by the ratio-form
267    /// term recurrence.
268    fn iacoth<const B: Word>(&self, n: IBig) -> FBig<R, B> {
269        let n: u32 = (&n).try_into().expect("iacoth argument must fit in u32");
270
271        // number of series terms until r_k < B^{-p}:  (2k+1)·log_B(n) > p.
272        // The count is generously over-provisioned, so a truncating cast stands in
273        // for a ceiling.
274        let log_b_n = n.log2_est() / B.log2_est();
275        let num_terms = (self.precision as f32 / (2.0 * log_b_n)) as usize + 10;
276
277        let (_p, q, t) = crate::math::cache::iacoth_bs(n, 1, num_terms + 1);
278
279        // L(n) = (Q + T) / (n·Q). Extra guard digits absorb the division's rounding
280        // (the binary-splitting state is exact, so only this single round loses anything).
281        let guard_digits = self.base_guard_digits::<B>();
282        let work_context = Self::new(self.precision + guard_digits + 2);
283
284        let num = work_context.convert_int::<B>(q.as_ibig() + &t).value();
285        let denom = work_context.convert_int::<B>(IBig::from(n) * &q).value();
286        num / denom
287    }
288
289    /// Evaluate `ln(x)` (or `ln(x+1)` when `one_plus`) at `work_precision` via the atanh series,
290    /// returning a [`Ball`] whose radius is derived mechanically by Ball arithmetic (error
291    /// propagates term-by-term through the series; cancellation and the `s·ln(B)` reconstruction
292    /// flow through the Ball scale factors).
293    ///
294    /// This is the near-correct computation core shared by the public Ziv-backed `ln`/`ln_1p`
295    /// (which wrap it in a retry loop) and by `ln_base` (which only needs a near-correct constant
296    /// `ln(B)`). It lives on `R: Round` so those near-correct callers don't inherit the
297    /// `ErrorBounds` bound.
298    ///
299    /// The magnitude is reduced onto the unit binade in two exact stages (a base-power
300    /// exponent re-tag, then a small power-of-two division), so no power of an exponent *gap*
301    /// is ever materialized — the reduction stays O(1) for any input exponent.
302    ///
303    /// The reconstruction `2·sum + s2·ln(2) + e_base·ln(B)` cancels for x < 1, so the series
304    /// runs at double working precision — decided *before* the series, since a value-space
305    /// radius needs no re-tagging when the precision changes (the mechanism the old ulp-count
306    /// `rescale_precision` existed for is gone entirely).
307    pub(crate) fn ln_compute<const B: Word>(
308        &self,
309        x: &Repr<B>,
310        mut work_precision: usize,
311        one_plus: bool,
312        mut cache: Option<&mut ConstCache>,
313    ) -> Result<Ball<B>, FpError> {
314        // Round the input to the working precision; the input's own rounding is the only error
315        // introduced here.
316        let context = Context::<mode::HalfEven>::new(work_precision);
317        let x_ball = Ball::from_rounded(context.repr_round_ref(x), work_precision);
318
319        // When one_plus is true and |x| < 1/B, the input is fed into the Maclaurin without scaling.
320        let no_scaling = one_plus && x_ball.mid.log2_est() < -B.log2_est();
321
322        // The magnitude is reduced onto the unit binade in two exact stages, so that no power
323        // of the exponent *gap* is ever materialized (a gap of ~10^9 digits would otherwise
324        // allocate and divide integers of ~10^9 bits — `ln(1e1000000000)` never returned).
325        //
326        // 1. Split at a base-power boundary — a pure exponent re-tag (O(1), `Ball::shift`):
327        //    with `d = digits_ub(sig) − 1`, the mid becomes `sig·B^(−d) ∈ [B^(−2), B)` and the
328        //    removed factor is `B^(e+d)`. Unlike a single power-of-two reduction over the whole
329        //    magnitude, this stage is exact integer exponent arithmetic; the f32 `log2_bounds`
330        //    estimate loses ~hundreds of *bits* of accuracy once |log2 x| approaches 10^9, so
331        //    deriving the whole reduction from it left the scaled value far outside [1, 2).
332        //    Applied only for |e| ≥ 2: within |e| ≤ 1 the power-of-two reduction below shifts by
333        //    at most `log2(sig) + 2·log2(B)` bits — proportional to the input — and (crucially)
334        //    needs no `ln(B)` term in the reconstruction. That matters because `ln(B)` itself
335        //    normalizes to `1·B^1`, so a base split of it would recurse onto its own
336        //    reconstruction term (an infinite recursion on generic bases).
337        // 2. Finish the reduction onto [1, 2) by a power of two: within this bounded range the
338        //    f32 floor is off by at most one, and `2^|s2|` is a couple of words at most.
339        //
340        let (s2, e_base, x_scaled, below_one) = if no_scaling {
341            (0, 0, x_ball, false)
342        } else {
343            let x_ball = if one_plus {
344                x_ball.add(&Ball::exact_int(IBig::ONE, work_precision), work_precision)?
345            } else {
346                x_ball
347            };
348            // A zero midpoint means the `ln_1p` argument rounded onto the pole at `−1` and the sum
349            // cancelled exactly (`1 + x` is within `rad` of zero, so it may as well be negative —
350            // no logarithm exists). `log2_bounds` saturates at `−∞` there, which the reduction
351            // below cannot use. The whole-line radius asks the Ziv loop for a higher guard, at
352            // which the argument is representable and the sum is a nonzero difference again.
353            if x_ball.mid.significand().is_zero() {
354                return Ok(Ball::with_error(Repr::zero(), Mag::INFINITY));
355            }
356            // Does the (rounded) input sit below one? The exact cancellation condition for
357            // the reconstruction below — one cheap comparison, immune to how the two-stage
358            // reduction happens to split the exponent (master's single-stage `s < 0` read
359            // the same predicate off its split).
360            let below_one = x_ball.mid.cmp(&Repr::<B>::one()) == Ordering::Less;
361
362            let e = x_ball.mid.exponent();
363            let (e_base, x1) = if e >= 2 || e <= -2 {
364                let d = x_ball.mid.digits_ub() as isize - 1;
365                let e_base = e.saturating_add(d);
366                (e_base, x_ball.shift(-e_base)) // value sig·B^(−d) ∈ [B^(−2), B)
367            } else {
368                (0, x_ball)
369            };
370
371            let log2 = x1.mid.log2_bounds().0;
372            // The f32 bound is `next_down`-ed, so a midpoint that is *exactly* a power of two
373            // (common: the base split above lands `B^e` inputs on `x1 = 1.0`) floors one step
374            // too low — `s2 = −1` would scale the ball up to `2` for nothing. An exact
375            // comparison settles the power-of-two boundary.
376            let mut s2 = log2 as isize - (log2 < 0.) as isize; // floor(log2(x1))
377            if s2 < 0 && x1.mid.cmp(&Repr::<B>::one()) != Ordering::Less {
378                s2 = 0;
379            }
380            let x_scaled = if s2 > 0 {
381                // Divide by the exact power 2^s2 as a ball division: an exact divisor shrinks
382                // the radius by its value (`rad_b = 0` in the division rule) — no special case.
383                let k = Ball::exact(Repr::new(IBig::ONE << s2 as usize, 0));
384                x1.div(&k, work_precision)?
385            } else if s2 < 0 {
386                // Scaling by 2^|s2| is exact (finite decimal × power of two); an exact operand
387                // keeps `rad = 0` through the multiplication's conditional ε.
388                x1.scale_int(&(IBig::ONE << (-s2) as usize), work_precision)?
389            } else {
390                x1
391            };
392            debug_assert!(x_scaled.mid.cmp(&Repr::<B>::one()) != Ordering::Less);
393            (s2, e_base, x_scaled, below_one)
394        };
395
396        // The reconstruction `2·sum + s2·ln(2) + e_base·ln(B)` *cancels* when the exact terms
397        // of the scale factors dominate the result — precisely when the input is below one
398        // (the series sum is then small against the constant terms), so the series runs at
399        // double precision to keep the pre-cancellation sum accurate. A split with `s2 < 0`
400        // but `x ≥ 1` (e.g. the power-two stage landing `x1 = 0.999…` below one) does not
401        // cancel and stays at single precision.
402        if below_one || x_scaled.mid.sign() == Sign::Negative {
403            work_precision += self.precision;
404        }
405
406        // after the number is scaled to nearly one, use Maclaurin series on log(x) = 2atanh(z):
407        // let z = (x-1)/(x+1) < 1, log(x) = 2atanh(z) = 2Σ(z²ⁱ⁺¹/(2i+1)) for i = 1,3,5,...
408        let z = if no_scaling {
409            let two = Ball::exact_int(IBig::from(2), work_precision);
410            let den = x_scaled.add(&two, work_precision)?;
411            x_scaled.div(&den, work_precision)?
412        } else {
413            let one = Ball::exact_int(IBig::ONE, work_precision);
414            let num = x_scaled.sub(&one, work_precision)?;
415            let den = x_scaled.add(&one, work_precision)?;
416            num.div(&den, work_precision)?
417        };
418        let z2 = z.mul(&z, work_precision)?;
419        let mut pow = z.clone();
420        let mut sum = z;
421        let mut k: usize = 3;
422        loop {
423            pow = pow.mul(&z2, work_precision)?;
424
425            let increase = pow.div_int(k, work_precision)?;
426            if increase.mid_le_ulp_lb(&sum, work_precision) {
427                break;
428            }
429
430            sum = sum.add(&increase, work_precision)?;
431            k += 2;
432        }
433
434        // Omitted series tail: the first omitted term is ≤ sum.ulp_lb(), and the tail of the
435        // atanh series shrinks by z² per step with 1/(1−z²) < B for x_scaled ∈ [1, B), so the
436        // tail is < B·sum.ulp_lb() < B ulps of sum. A zero sum means z (hence every term) is
437        // exactly zero — the tail is 0.
438        if !sum.mid.significand().is_zero() {
439            sum.add_error(ulps::<B>(&sum.mid, work_precision, B as usize));
440        }
441
442        // Compose the logarithm of the original number: ln(x) = 2·sum + s2·ln(2) + e_base·ln(B).
443        // The constants carry a handful of ulps of error (8 is a conservative sound bound for
444        // every code path, cached and uncached), and the exact integer scale factors amplify
445        // that error by |s2|/|e_base| while the result's ulp grows only by B^log_B(|scale|) —
446        // so the constants are evaluated with enough extra digits to keep the amplified radius
447        // below one work-ulp divided by the constant's own 8-ulp slack — i.e. `8/B` work-ulps,
448        // sub-ulp for the base-10 origin of the construction and a few work-ulps on the
449        // smallest bases (sound either way; Ziv certifies, and the initial guard absorbs it).
450        // (The same construction as `exp_compute`'s `extra`.) Without them, ln of a large base
451        // power sits at a sizable fraction of the target half-ulp and pays a Ziv retry
452        // whenever the work value also lands near a rounding boundary.
453        let sum2 = sum.scale_int(&IBig::from(2), work_precision)?;
454        if no_scaling {
455            Ok(sum2)
456        } else if B.is_power_of_two() {
457            // ln(B) = log2(B)·ln(2) exactly, so both scale factors fold into one ln(2) term.
458            let k = IBig::from(s2) + IBig::from(e_base) * IBig::from(B.trailing_zeros());
459            if k.is_zero() {
460                return Ok(sum2);
461            }
462            let const_prec = work_precision + const_extra_digits::<B>(k.bit_len());
463            let ln2 =
464                Context::<mode::HalfEven>::new(const_prec).ln2::<B>(reborrow_cache(&mut cache));
465            let rad = ulps::<B>(&ln2.repr, const_prec, 8);
466            let ln2_ball = Ball::with_error(ln2.into_repr(), rad);
467            sum2.add(&ln2_ball.scale_int(&k, const_prec)?, const_prec)
468        } else {
469            let scale = Ord::max(s2.unsigned_abs(), e_base.unsigned_abs());
470            let const_prec = work_precision + const_extra_digits::<B>(scale.bit_len());
471            let mut result = sum2;
472            if s2 != 0 {
473                let ln2 =
474                    Context::<mode::HalfEven>::new(const_prec).ln2::<B>(reborrow_cache(&mut cache));
475                let rad = ulps::<B>(&ln2.repr, const_prec, 8);
476                let ln2_ball = Ball::with_error(ln2.into_repr(), rad);
477                result =
478                    result.add(&ln2_ball.scale_int(&IBig::from(s2), const_prec)?, const_prec)?;
479            }
480            if e_base != 0 {
481                let ln_base_ball = Context::<mode::HalfEven>::new(const_prec)
482                    .ln_base_ball::<B>(reborrow_cache(&mut cache))?;
483                result = result
484                    .add(&ln_base_ball.scale_int(&IBig::from(e_base), const_prec)?, const_prec)?;
485            }
486            Ok(result)
487        }
488    }
489
490    /// `ln(1 + arg)` of a *ball* input. [`ln_compute`](Self::ln_compute) evaluates the series on
491    /// `arg.mid`; the input ball's own error `|θ| ≤ arg.rad` then contributes
492    /// `|θ| / LB(1 + arg_lo)` to the log — the derivative bound at the ball's lower endpoint,
493    /// with the endpoint built by exact `Repr` arithmetic (the log1p rule as validated in
494    /// dashu-ball, tighter than the old `×2` ulp-domain formula). An endpoint crossing zero
495    /// means the ball straddles the pole: the whole-line radius forces a Ziv retry at higher
496    /// precision.
497    pub(crate) fn ln_1p_ball<const B: Word>(
498        &self,
499        arg: &Ball<B>,
500        mut cache: Option<&mut ConstCache>,
501    ) -> Result<Ball<B>, FpError> {
502        // The ball must stay clear of the pole at `1 + arg = 0`: there is no logarithm to compute
503        // (a zero sum has no `log2_bounds`, and a negative one is out of domain), and the
504        // derivative fold below is unbounded. The whole-line radius asks the Ziv loop for a higher
505        // guard, where the caller's argument stops rounding onto the pole — `atanh`'s `2x/(1−x)`
506        // reaching `±∞` at the first attempt is exactly this case. A whole-line *radius* (another
507        // operator's retry signal, e.g. a root that rounded onto zero) is the same situation.
508        if !arg.mid.is_finite() || arg.rad.is_infinite() {
509            return Ok(Ball::with_error(Repr::zero(), Mag::INFINITY));
510        }
511        let lo = &(Repr::<B>::one() + &arg.mid) - &arg.rad.to_repr::<B>();
512        let denom = if lo.sign() == Sign::Negative {
513            Mag::ZERO
514        } else {
515            Mag::from_repr_lower(&lo)
516        };
517        if denom.is_zero() {
518            return Ok(Ball::with_error(Repr::zero(), Mag::INFINITY));
519        }
520        let mut ln_ball =
521            self.ln_compute::<B>(&arg.mid, self.precision, true, reborrow_cache(&mut cache))?;
522        ln_ball.add_error(arg.rad.div(&denom));
523        Ok(ln_ball)
524    }
525}
526
527// `ln`/`ln_1p` are correctly rounded via the Ziv loop, whose containment test needs the rounding
528// preimage (`R: ErrorBounds`). They delegate the series to `ln_compute`.
529impl<R: ErrorBounds> Context<R> {
530    /// Calculate the natural logarithm function (`log(x)`) on the float number under this context.
531    ///
532    /// # Examples
533    ///
534    /// ```
535    /// # use core::str::FromStr;
536    /// # use dashu_base::ParseError;
537    /// # use dashu_float::DBig;
538    /// use dashu_base::Approximation::*;
539    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
540    ///
541    /// let context = Context::<HalfAway>::new(2);
542    /// let a = DBig::from_str("1.234")?;
543    /// assert_eq!(context.ln(&a.repr(), None), Ok(Inexact(DBig::from_str("0.21")?, NoOp)));
544    /// # Ok::<(), ParseError>(())
545    /// ```
546    #[inline]
547    pub fn ln<const B: Word>(
548        &self,
549        x: &Repr<B>,
550        cache: Option<&mut ConstCache>,
551    ) -> FpResult<FBig<R, B>> {
552        if x.is_infinite() {
553            return Err(FpError::InfiniteInput);
554        }
555        if x.significand.is_zero() {
556            // ln(±0) = -inf (a value, not an error)
557            return Ok(Exact(FBig::new(Repr::neg_infinity(), *self)));
558        }
559        if x.sign() == Sign::Negative {
560            return Err(FpError::OutOfDomain);
561        }
562        self.ln_internal(x, false, cache)
563    }
564
565    /// Calculate the natural logarithm function (`log(x+1)`) on the float number under this context.
566    ///
567    /// # Examples
568    ///
569    /// ```
570    /// # use core::str::FromStr;
571    /// # use dashu_base::ParseError;
572    /// # use dashu_float::DBig;
573    /// use dashu_base::Approximation::*;
574    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
575    ///
576    /// let context = Context::<HalfAway>::new(2);
577    /// let a = DBig::from_str("0.1234")?;
578    /// assert_eq!(context.ln_1p(&a.repr(), None), Ok(Inexact(DBig::from_str("0.12")?, AddOne)));
579    /// # Ok::<(), ParseError>(())
580    /// ```
581    #[inline]
582    pub fn ln_1p<const B: Word>(
583        &self,
584        x: &Repr<B>,
585        cache: Option<&mut ConstCache>,
586    ) -> FpResult<FBig<R, B>> {
587        if x.is_infinite() {
588            return Err(FpError::InfiniteInput);
589        }
590        // Domain of ln_1p is x > -1. x == -1 gives -inf; x < -1 is out of domain. `-1` is a pole,
591        // so the boundary is compared *exactly* (no precision argument): `-1 + 10⁻¹⁵⁵` at 100
592        // digits rounds onto `-1` but has the finite value `ln(10⁻¹⁵⁵)`.
593        if x.sign() == Sign::Negative && !x.significand.is_zero() {
594            match repr_cmp_same_base::<B, true>(x, &Repr::<B>::one(), None) {
595                Ordering::Greater => return Err(FpError::OutOfDomain), // x < -1
596                Ordering::Equal => return Ok(Exact(FBig::new(Repr::neg_infinity(), *self))),
597                _ => {}
598            }
599        }
600        self.ln_internal(x, true, cache)
601    }
602
603    fn ln_internal<const B: Word>(
604        &self,
605        x: &Repr<B>,
606        one_plus: bool,
607        mut cache: Option<&mut ConstCache>,
608    ) -> FpResult<FBig<R, B>> {
609        assert_finite(x);
610
611        // Exact special cases first: they need no rounding, so a precision-0 (unlimited)
612        // value such as `FBig::ONE` or the one from `try_from(0.0)` must still resolve
613        // ln/ln_1p exactly rather than tripping the limited-precision assertion below.
614        if !one_plus && x.is_one() {
615            return Ok(Exact(FBig::ZERO)); // ln(1) = +0
616        }
617        if one_plus && x.significand.is_zero() {
618            // ln_1p(±0) = ±0
619            let zero = if x.is_neg_zero() {
620                FBig::new(Repr::neg_zero(), *self)
621            } else {
622                FBig::ZERO
623            };
624            return Ok(Exact(zero));
625        }
626
627        assert_limited_precision(self.precision);
628
629        // Correct rounding via the Ziv loop: `ln_compute` evaluates the atanh series at `p + guard`
630        // and reports a provable error radius; the driver retries with more guard digits until the
631        // approximation's error interval lies entirely inside one rounding bin. The guard is a
632        // *performance* knob (first-attempt hit rate), not a correctness backstop — Ziv certifies
633        // the result. (The pre-Ziv `+ 2` is retained: with the conservative radius below it is still
634        // needed for the first attempt to clear the half-ulp preimage at typical precisions.)
635        let base_guard = self.base_guard_digits::<B>() + 2;
636        self.ziv(base_guard + one_plus as usize, |guard| {
637            Ok(self
638                .ln_compute::<B>(x, self.precision + guard, one_plus, reborrow_cache(&mut cache))?
639                .to_value_radius::<R>(&Context::<R>::new(self.precision + guard)))
640        })
641    }
642
643    /// Calculate the base-2 logarithm (`log2(x)`) on the float number under this context.
644    ///
645    /// Correctly rounded to the context's precision under any rounding mode; for an exact power
646    /// of two the result is the exact integer `log2(x)`.
647    ///
648    /// # Domain
649    ///
650    /// `log2(±0) = −∞` and a negative (non-zero) input is out of domain; an infinite input is an
651    /// error (a finite context cannot produce the infinite `log2(+∞) = +∞` exactly).
652    ///
653    /// # Examples
654    ///
655    /// ```
656    /// # use core::str::FromStr;
657    /// # use dashu_base::ParseError;
658    /// # use dashu_float::DBig;
659    /// use dashu_base::Approximation::*;
660    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
661    ///
662    /// let context = Context::<HalfAway>::new(4);
663    /// let a = DBig::from_str("10")?;
664    /// assert_eq!(context.log2(&a.repr(), None), Ok(Inexact(DBig::from_str("3.322")?, AddOne)));
665    /// # Ok::<(), ParseError>(())
666    /// ```
667    #[inline]
668    pub fn log2<const B: Word>(
669        &self,
670        x: &Repr<B>,
671        cache: Option<&mut ConstCache>,
672    ) -> FpResult<FBig<R, B>> {
673        if x.is_infinite() {
674            return Err(FpError::InfiniteInput);
675        }
676        if x.significand.is_zero() {
677            // log2(±0) = -inf (a value, not an error)
678            return Ok(Exact(FBig::new(Repr::neg_infinity(), *self)));
679        }
680        if x.sign() == Sign::Negative {
681            return Err(FpError::OutOfDomain);
682        }
683        self.log2_internal(x, cache)
684    }
685
686    fn log2_internal<const B: Word>(
687        &self,
688        x: &Repr<B>,
689        mut cache: Option<&mut ConstCache>,
690    ) -> FpResult<FBig<R, B>> {
691        assert_finite(x);
692
693        // Exact shortcuts first — they also cover unlimited precision, which the Ziv loop below
694        // rejects via its limited-precision assertion.
695        if x.is_one() {
696            return Ok(Exact(FBig::ZERO)); // log2(1) = +0
697        }
698
699        // Exact power-of-two shortcut: if x = 2^k for an integer k, log2(x) = k. This is *required*
700        // for directed rounding — the Ziv loop below cannot certify an exactly-representable
701        // result whose true value sits on a rounding boundary (its shrinking error interval
702        // always straddles the boundary), so without this shortcut log2(2^-159) under `Up` would
703        // exhaust the retry cap and return k + 1 ulp instead of the exact k.
704        //
705        // log2(x) = log2(significand) + exponent·log2(B). With significand = 2^m this is an exact
706        // integer whenever log2(B) is integral (B a power of two), or — for a non-power-of-two
707        // base — when the exponent is zero.
708        let mag = (&x.significand).unsigned_abs();
709        if mag.is_power_of_two() && (x.exponent == 0 || B.is_power_of_two()) {
710            let m = mag.trailing_zeros().unwrap(); // = log2(significand)
711            let log2_b = B.trailing_zeros() as isize;
712            let k = IBig::from(m) + IBig::from(x.exponent) * IBig::from(log2_b);
713            return Ok(self.convert_int::<B>(k));
714        }
715
716        assert_limited_precision(self.precision);
717
718        // log2(x) = ln(x)/ln(2), correctly rounded via the Ziv loop. Both logarithms come from the
719        // Ball-based `ln_compute`, and dividing them as Balls composes the radius mechanically:
720        // the quotient's error is bounded from the two logarithms' relative errors, with no
721        // directed-interval bookkeeping or guard-digit constant. The driver certifies the result
722        // against the rounding preimage exactly as before.
723        let initial_guard = self.base_guard_digits::<B>() + 4;
724        self.ziv(initial_guard, |guard| {
725            let work_precision = self.precision + guard;
726            let lx = self.ln_compute::<B>(x, work_precision, false, reborrow_cache(&mut cache))?;
727            let two = Repr::new(IBig::from(2), 0);
728            let l2 =
729                self.ln_compute::<B>(&two, work_precision, false, reborrow_cache(&mut cache))?;
730            Ok(lx
731                .div(&l2, work_precision)?
732                .to_value_radius::<R>(&Context::<R>::new(work_precision)))
733        })
734    }
735
736    /// Calculate the base-10 logarithm (`log10(x)`) on the float number under this context.
737    ///
738    /// Correctly rounded to the context's precision under any rounding mode; for an exact power
739    /// of ten the result is the exact integer `log10(x)`.
740    ///
741    /// # Domain
742    ///
743    /// `log10(±0) = −∞` and a negative (non-zero) input is out of domain; an infinite input is an
744    /// error (a finite context cannot produce the infinite `log10(+∞) = +∞` exactly).
745    ///
746    /// # Examples
747    ///
748    /// ```
749    /// # use core::str::FromStr;
750    /// # use dashu_base::ParseError;
751    /// # use dashu_float::DBig;
752    /// use dashu_base::Approximation::*;
753    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
754    ///
755    /// let context = Context::<HalfAway>::new(4);
756    /// let a = DBig::from_str("100")?;
757    /// assert_eq!(context.log10(&a.repr(), None), Ok(Exact(DBig::from_str("2")?)));
758    /// # Ok::<(), ParseError>(())
759    /// ```
760    #[inline]
761    pub fn log10<const B: Word>(
762        &self,
763        x: &Repr<B>,
764        cache: Option<&mut ConstCache>,
765    ) -> FpResult<FBig<R, B>> {
766        if x.is_infinite() {
767            return Err(FpError::InfiniteInput);
768        }
769        if x.significand.is_zero() {
770            // log10(±0) = -inf (a value, not an error)
771            return Ok(Exact(FBig::new(Repr::neg_infinity(), *self)));
772        }
773        if x.sign() == Sign::Negative {
774            return Err(FpError::OutOfDomain);
775        }
776        self.log10_internal(x, cache)
777    }
778
779    fn log10_internal<const B: Word>(
780        &self,
781        x: &Repr<B>,
782        mut cache: Option<&mut ConstCache>,
783    ) -> FpResult<FBig<R, B>> {
784        assert_finite(x);
785
786        // Exact shortcuts first — they also cover unlimited precision, which the Ziv loop below
787        // rejects via its limited-precision assertion.
788        if x.is_one() {
789            return Ok(Exact(FBig::ZERO)); // log10(1) = +0
790        }
791
792        // Exact power-of-ten shortcut: if x = 10^m for an integer m, log10(x) = m. This is *required*
793        // for directed rounding — the Ziv loop below cannot certify an exactly-representable result
794        // whose true value sits on a rounding boundary (its shrinking error interval always
795        // straddles the one-sided preimage), so without this shortcut log10(10^-159) under `Up`
796        // would exhaust the retry cap and return m + 1 ulp instead of the exact m.
797        if let Some(m) = exact_pow10_log::<B>(&x.significand, x.exponent) {
798            return Ok(self.convert_int::<B>(IBig::from(m)));
799        }
800
801        assert_limited_precision(self.precision);
802
803        // log10(x) = ln(x)/ln(10), correctly rounded via the Ziv loop. Both logarithms come from the
804        // Ball-based `ln_compute`, and dividing them as Balls composes the radius mechanically:
805        // the quotient's error is bounded from the two logarithms' relative errors, with no
806        // directed-interval bookkeeping or guard-digit constant. The driver certifies the result
807        // against the rounding preimage exactly as before.
808        let initial_guard = self.base_guard_digits::<B>() + 4;
809        self.ziv(initial_guard, |guard| {
810            let work_precision = self.precision + guard;
811            let lx = self.ln_compute::<B>(x, work_precision, false, reborrow_cache(&mut cache))?;
812            let ten = Repr::new(IBig::from(10), 0);
813            let l10 =
814                self.ln_compute::<B>(&ten, work_precision, false, reborrow_cache(&mut cache))?;
815            Ok(lx
816                .div(&l10, work_precision)?
817                .to_value_radius::<R>(&Context::<R>::new(work_precision)))
818        })
819    }
820}
821
822/// If `x = sig·B^e` is exactly `10^m` for some integer `m`, return `m`; otherwise `None`.
823///
824/// `10^m = 2^m·5^m`, so `x` is a power of ten iff the 2-valuation and 5-valuation of `sig·B^e`
825/// coincide and `x` has no other prime factor. [`UBig::remove_word`] divides out all 2s and 5s from
826/// both the base and the significand, returning each valuation as the removed exponent (and leaving
827/// any non-{2,5} prime factor behind as a cofactor ≠ 1). The base `B = 2^p·5^q·s` (with `s` coprime
828/// to 10) contributes `p·e` to the 2-valuation and `q·e` to the 5-valuation, and `s` must not
829/// appear (unless `e = 0`). The valuations may be negative (`x` a negative power of ten).
830fn exact_pow10_log<const B: Word>(sig: &IBig, e: isize) -> Option<isize> {
831    // base: divide out all 2s and 5s, getting (p, q, leftover)
832    let mut rest = UBig::from_word(B);
833    let p = rest.remove_word(2)? as isize; // B ≥ 2, so never None
834    let q = rest.remove_word(5).unwrap() as isize;
835    // the leftover would give `x` a non-{2,5} prime factor when e ≠ 0
836    if !rest.is_one() && e != 0 {
837        return None;
838    }
839
840    // significand: divide out all 2s and 5s, getting (v2, v5, cofactor)
841    let mut sig_abs = sig.unsigned_abs();
842    let v2_sig = sig_abs.remove_word(2)? as isize; // non-zero upstream, so never None
843    let v5 = sig_abs.remove_word(5).unwrap() as isize;
844    // after removing every 2 and 5 the significand must be 1 (no other prime factor)
845    if sig_abs != UBig::ONE {
846        return None;
847    }
848
849    // `p*e` / `q*e` can overflow `isize` for an exotic base (B a large power of 2) at extreme
850    // exponents on 32-bit; the true log10 would not fit `isize` then, so bail rather than compare
851    // wrapped values that could falsely match.
852    let v2 = v2_sig.checked_add(p.checked_mul(e)?)?;
853    let v5 = v5.checked_add(q.checked_mul(e)?)?;
854    (v2 == v5).then_some(v2)
855}
856
857#[cfg(test)]
858mod tests {
859    use super::*;
860    use crate::ball::ulp_mag;
861    use crate::round::mode;
862    use alloc::vec::Vec;
863    use dashu_base::{Abs, AbsOrd};
864
865    #[test]
866    fn test_log10_domain() {
867        let ctx = Context::<mode::HalfEven>::new(53);
868        // log10(±0) = -inf (a value, not an error)
869        let r = ctx.log10::<2>(&Repr::<2>::zero(), None).unwrap().value();
870        assert!(r.repr.is_infinite());
871        assert_eq!(r.repr.sign(), Sign::Negative);
872        // log10(negative) is out of domain
873        assert!(matches!(
874            ctx.log10::<2>(&Repr::new((-1).into(), 0), None),
875            Err(FpError::OutOfDomain)
876        ));
877        // an infinite input is rejected
878        assert!(matches!(ctx.log10::<2>(&Repr::infinity(), None), Err(FpError::InfiniteInput)));
879    }
880
881    #[test]
882    fn test_log10_exact_power_of_ten() {
883        // log10(10^k) = k exactly under every rounding mode. Regression for the directed-rounding
884        // defect the power-of-ten shortcut exists for: rounding ln(x) and ln(10) each toward the
885        // mode and dividing once does not bound the quotient, so previously log10(10^-159) under
886        // `Up` returned -159 + 1 ulp.
887        let p = 53;
888        for k in [0isize, 1, -1, 5, -159, 1000, -1000] {
889            let x = Repr::<10>::new(IBig::from(1), k); // 10^k (base 10: significand 1)
890            let r_down = Context::<mode::Down>::new(p)
891                .log10::<10>(&x, None)
892                .unwrap()
893                .value();
894            let r_up = Context::<mode::Up>::new(p)
895                .log10::<10>(&x, None)
896                .unwrap()
897                .value();
898            let r_zero = Context::<mode::Zero>::new(p)
899                .log10::<10>(&x, None)
900                .unwrap()
901                .value();
902            let r_he = Context::<mode::HalfEven>::new(p)
903                .log10::<10>(&x, None)
904                .unwrap()
905                .value();
906            assert_eq!(r_down.repr, r_he.repr, "Down != HalfEven for log10(10^{k})");
907            assert_eq!(r_up.repr, r_he.repr, "Up != HalfEven for log10(10^{k})");
908            assert_eq!(r_zero.repr, r_he.repr, "Zero != HalfEven for log10(10^{k})");
909            assert_eq!(r_he.to_int().value(), IBig::from(k), "value for log10(10^{k})");
910        }
911    }
912
913    #[test]
914    fn test_exact_pow10_log_overflow() {
915        // `p*e` overflows isize for a base that is a large power of two at an extreme exponent
916        // (on 32-bit and 64-bit alike); the exact log wouldn't fit isize, so it must be None, not
917        // a wrapped value that could falsely compare equal and return a wrong `Some`.
918        assert_eq!(exact_pow10_log::<256>(&IBig::ONE, isize::MAX), None);
919        assert_eq!(exact_pow10_log::<256>(&IBig::ONE, isize::MIN), None);
920        // sanity: the normal path is unaffected.
921        assert_eq!(exact_pow10_log::<10>(&IBig::ONE, 5), Some(5));
922    }
923
924    #[test]
925    fn test_log10_exact_power_of_ten_binary_base() {
926        // In base 2, 10^k is a float only via a 5^k-significand (e.g. 100 = 25·2^2); the
927        // valuation-based shortcut must still detect the exact log10.
928        let p = 53;
929        for (sig, e, want) in [(25i32, 2isize, 2i64), (5, 1, 1), (125, 3, 3), (50, 1, 2)] {
930            // 50·2^1 = 100 = 10^2 too (a non-normalized significand)
931            let x = Repr::<2>::new(IBig::from(sig), e);
932            let r_down = Context::<mode::Down>::new(p)
933                .log10::<2>(&x, None)
934                .unwrap()
935                .value();
936            let r_up = Context::<mode::Up>::new(p)
937                .log10::<2>(&x, None)
938                .unwrap()
939                .value();
940            let r_zero = Context::<mode::Zero>::new(p)
941                .log10::<2>(&x, None)
942                .unwrap()
943                .value();
944            let r_he = Context::<mode::HalfEven>::new(p)
945                .log10::<2>(&x, None)
946                .unwrap()
947                .value();
948            assert_eq!(r_down.to_int().value(), IBig::from(want), "log10({sig}·2^{e}) Down");
949            assert_eq!(r_up.to_int().value(), IBig::from(want), "log10({sig}·2^{e}) Up");
950            assert_eq!(r_zero.to_int().value(), IBig::from(want), "log10({sig}·2^{e}) Zero");
951            assert_eq!(r_he.to_int().value(), IBig::from(want), "log10({sig}·2^{e}) HalfEven");
952        }
953    }
954
955    #[test]
956    fn test_log10_fbig_convenience() {
957        // FBig::log10 convenience layer: exact powers of ten in both bases.
958        let x = FBig::<mode::HalfEven, 10>::from_repr(Repr::new(1000.into(), 0), Context::new(50));
959        assert_eq!(x.log10(), FBig::<mode::HalfEven, 10>::from(3u8));
960        // base-2 float 100 = 25·2^2
961        let x = FBig::<mode::HalfEven, 2>::from_repr(Repr::new(25.into(), 2), Context::new(50));
962        assert_eq!(x.log10(), FBig::<mode::HalfEven, 2>::from(2u8));
963    }
964
965    /// Fixed inputs for the `log10` oracle differential.
966    fn log10_diff_inputs() -> Vec<Repr<2>> {
967        let mut v = Vec::new();
968        for x in [0.5f64, 1.5, 2.0, 3.0, 10.0, 1000.0, 1e-6, 123.456, 2.5e-10] {
969            v.push(FBig::<mode::HalfEven, 2>::try_from(x).unwrap().into_repr());
970        }
971        for k in [-100isize, -50, -10, -1, 0, 1, 10, 50, 100] {
972            v.push(Repr::new(IBig::ONE, k)); // 2^k
973        }
974        v.push(
975            FBig::<mode::HalfEven, 2>::try_from(f64::MAX)
976                .unwrap()
977                .into_repr(),
978        );
979        v
980    }
981
982    fn check_log10_differential<R: ErrorBounds>(p: usize, x: &Repr<2>, oracle: &Repr<2>) {
983        let ctx = Context::<R>::new(p);
984        let want = ctx.repr_round_ref(oracle).value();
985        let got = ctx.log10_internal::<2>(x, None).unwrap().value();
986        assert_eq!(got.repr, want, "p={p} {} x={x:?}", core::any::type_name::<R>(),);
987    }
988
989    #[test]
990    fn log10_ball_matches_oracle() {
991        let inputs = log10_diff_inputs();
992        for p in [20usize, 50, 100] {
993            for x in &inputs {
994                let oracle = Context::<mode::HalfEven>::new(p + 60)
995                    .log10::<2>(x, None)
996                    .unwrap()
997                    .value();
998                check_log10_differential::<mode::HalfEven>(p, x, &oracle.repr);
999                check_log10_differential::<mode::Down>(p, x, &oracle.repr);
1000                check_log10_differential::<mode::Up>(p, x, &oracle.repr);
1001                check_log10_differential::<mode::Zero>(p, x, &oracle.repr);
1002                check_log10_differential::<mode::Away>(p, x, &oracle.repr);
1003            }
1004        }
1005        // the arbitrary-precision regime: a reduced sweep (directed modes still exercised).
1006        for x in inputs.iter().step_by(9) {
1007            let oracle = Context::<mode::HalfEven>::new(560)
1008                .log10::<2>(x, None)
1009                .unwrap()
1010                .value();
1011            check_log10_differential::<mode::HalfEven>(500, x, &oracle.repr);
1012            check_log10_differential::<mode::Down>(500, x, &oracle.repr);
1013            check_log10_differential::<mode::Up>(500, x, &oracle.repr);
1014        }
1015    }
1016
1017    #[test]
1018    fn test_ln_zero_is_neg_infinity() {
1019        let ctx = Context::<mode::HalfEven>::new(53);
1020        let r = ctx.ln::<2>(&Repr::<2>::zero(), None).unwrap().value();
1021        assert!(r.repr().is_infinite());
1022        assert_eq!(r.repr().sign(), Sign::Negative);
1023    }
1024
1025    #[test]
1026    fn test_iacoth() {
1027        let context = Context::<mode::Zero>::new(10);
1028        let binary_6 = context.iacoth::<2>(6.into()).with_precision(10).value();
1029        assert_eq!(binary_6.repr.significand, IBig::from(689));
1030        let decimal_6 = context.iacoth::<10>(6.into()).with_precision(10).value();
1031        assert_eq!(decimal_6.repr.significand, IBig::from(1682361183));
1032
1033        let context = Context::<mode::Zero>::new(40);
1034        let decimal_6 = context.iacoth::<10>(6.into()).with_precision(40).value();
1035        assert_eq!(
1036            decimal_6.repr.significand,
1037            IBig::from_str_radix("1682361183106064652522967051084960450557", 10).unwrap()
1038        );
1039
1040        let context = Context::<mode::Zero>::new(201);
1041        let binary_6 = context.iacoth::<2>(6.into()).with_precision(201).value();
1042        assert_eq!(
1043            binary_6.repr.significand,
1044            IBig::from_str_radix(
1045                "2162760151454160450909229890833066944953539957685348083415205",
1046                10
1047            )
1048            .unwrap()
1049        );
1050    }
1051
1052    #[test]
1053    fn test_ln2_ln10() {
1054        let context = Context::<mode::Zero>::new(45);
1055        let decimal_ln2 = context.ln2::<10>(None).with_precision(45).value();
1056        assert_eq!(
1057            decimal_ln2.repr.significand,
1058            IBig::from_str_radix("693147180559945309417232121458176568075500134", 10).unwrap()
1059        );
1060        let decimal_ln10 = context.ln10::<10>(None).with_precision(45).value();
1061        assert_eq!(
1062            decimal_ln10.repr.significand,
1063            IBig::from_str_radix("230258509299404568401799145468436420760110148", 10).unwrap()
1064        );
1065
1066        let context = Context::<mode::Zero>::new(180);
1067        let binary_ln2 = context.ln2::<2>(None).with_precision(180).value();
1068        assert_eq!(
1069            binary_ln2.repr.significand,
1070            IBig::from_str_radix("1062244963371879310175186301324412638028404515790072203", 10)
1071                .unwrap()
1072        );
1073        let binary_ln10 = context.ln10::<2>(None).with_precision(180).value();
1074        assert_eq!(
1075            binary_ln10.repr.significand,
1076            IBig::from_str_radix("882175346869410758689845931257775553286341791676474847", 10)
1077                .unwrap()
1078        );
1079    }
1080
1081    #[test]
1082    fn test_log2_domain() {
1083        let ctx = Context::<mode::HalfEven>::new(53);
1084        // log2(±0) = -inf (a value, not an error)
1085        let r = ctx.log2::<2>(&Repr::<2>::zero(), None).unwrap().value();
1086        assert!(r.repr.is_infinite());
1087        assert_eq!(r.repr.sign(), Sign::Negative);
1088        // log2(negative) is out of domain
1089        assert!(matches!(
1090            ctx.log2::<2>(&Repr::new((-1).into(), 0), None),
1091            Err(FpError::OutOfDomain)
1092        ));
1093        // an infinite input is rejected
1094        assert!(matches!(ctx.log2::<2>(&Repr::infinity(), None), Err(FpError::InfiniteInput)));
1095    }
1096
1097    #[test]
1098    fn test_log2_exact_power_of_two() {
1099        // log2(2^k) = k exactly under every rounding mode. Regression for the directed-rounding
1100        // defect: rounding ln(x) and ln(2) each toward the mode and dividing once does not bound
1101        // the quotient, so previously log2(2^-159) under `Up` returned -159 + 1 ulp.
1102        let p = 53;
1103        for k in [0isize, 1, -1, 5, 159, -159, 1000, -1000] {
1104            let x = Repr::<2>::new(IBig::from(1), k); // 2^k
1105            let r_down = Context::<mode::Down>::new(p)
1106                .log2::<2>(&x, None)
1107                .unwrap()
1108                .value();
1109            let r_up = Context::<mode::Up>::new(p)
1110                .log2::<2>(&x, None)
1111                .unwrap()
1112                .value();
1113            let r_zero = Context::<mode::Zero>::new(p)
1114                .log2::<2>(&x, None)
1115                .unwrap()
1116                .value();
1117            let r_he = Context::<mode::HalfEven>::new(p)
1118                .log2::<2>(&x, None)
1119                .unwrap()
1120                .value();
1121            // Every directed mode produces the identical value — no mode-dependent ulp.
1122            assert_eq!(r_down.repr, r_he.repr, "Down != HalfEven for log2(2^{k})");
1123            assert_eq!(r_up.repr, r_he.repr, "Up != HalfEven for log2(2^{k})");
1124            assert_eq!(r_zero.repr, r_he.repr, "Zero != HalfEven for log2(2^{k})");
1125            // And that value is exactly k.
1126            assert_eq!(r_he.to_int().value(), IBig::from(k), "value for log2(2^{k})");
1127        }
1128    }
1129
1130    #[test]
1131    fn test_log2_exact_power_of_two_decimal_base() {
1132        // In a non-power-of-two base the shortcut still fires when the exponent is zero: a
1133        // significand that is itself a power of two makes x = 2^m exactly.
1134        let p = 53;
1135        for (sig, want) in [(8i32, 3isize), (1024, 10), (2, 1), (32, 5)] {
1136            let x = Repr::<10>::new(IBig::from(sig), 0);
1137            let r_down = Context::<mode::Down>::new(p)
1138                .log2::<10>(&x, None)
1139                .unwrap()
1140                .value();
1141            let r_up = Context::<mode::Up>::new(p)
1142                .log2::<10>(&x, None)
1143                .unwrap()
1144                .value();
1145            let r_he = Context::<mode::HalfEven>::new(p)
1146                .log2::<10>(&x, None)
1147                .unwrap()
1148                .value();
1149            assert_eq!(r_down.repr, r_he.repr, "Down != HalfEven for log2({sig}) base 10");
1150            assert_eq!(r_up.repr, r_he.repr, "Up != HalfEven for log2({sig}) base 10");
1151            assert_eq!(r_he.to_int().value(), IBig::from(want), "value for log2({sig}) base 10");
1152        }
1153    }
1154
1155    /// For a non-power-of-two significand `sig` (so `log2` is irrational and never lands on a
1156    /// rounding boundary), each directed result must equal a high-precision oracle rounded to the
1157    /// target precision under the same mode — the definition of correct rounding.
1158    fn check_log2_directed_matches_oracle<const B: Word>(sig: u32, p: usize) {
1159        let oracle_ctx = Context::<mode::HalfEven>::new(p + 40);
1160        let x = Repr::<B>::new(IBig::from(sig), 0);
1161        let oracle = oracle_ctx.log2::<B>(&x, None).unwrap().value();
1162
1163        let want_down = Context::<mode::Down>::new(p)
1164            .repr_round_ref(&oracle.repr)
1165            .value();
1166        let want_up = Context::<mode::Up>::new(p)
1167            .repr_round_ref(&oracle.repr)
1168            .value();
1169        let want_he = Context::<mode::HalfEven>::new(p)
1170            .repr_round_ref(&oracle.repr)
1171            .value();
1172
1173        let got_down = Context::<mode::Down>::new(p)
1174            .log2::<B>(&x, None)
1175            .unwrap()
1176            .value();
1177        let got_up = Context::<mode::Up>::new(p)
1178            .log2::<B>(&x, None)
1179            .unwrap()
1180            .value();
1181        let got_he = Context::<mode::HalfEven>::new(p)
1182            .log2::<B>(&x, None)
1183            .unwrap()
1184            .value();
1185
1186        assert_eq!(got_down.repr, want_down, "log2({sig}) base {B} under Down");
1187        assert_eq!(got_up.repr, want_up, "log2({sig}) base {B} under Up");
1188        assert_eq!(got_he.repr, want_he, "log2({sig}) base {B} under HalfEven");
1189    }
1190
1191    #[test]
1192    fn test_log2_directed_matches_oracle() {
1193        let p = 24;
1194        for sig in [3u32, 7, 10, 12345, 65537] {
1195            check_log2_directed_matches_oracle::<2>(sig, p);
1196        }
1197        // Exercise a non-power-of-two base through the Ziv interval path too.
1198        for sig in [3u32, 7, 10, 12345] {
1199            check_log2_directed_matches_oracle::<10>(sig, p);
1200        }
1201    }
1202
1203    // log2 of a value whose result sits within ~1 work-ulp of a power of two must still round to
1204    // the correct neighbor under directed modes. log2(f64::MAX) ≈ 1024 − 2^-53/ln2 sits just below
1205    // 1024; under Down at p=53 the answer is 1024 − 2^-42 (the largest p=53 value ≤ it), but an
1206    // unsound radius previously let Ziv certify 1024 on the first attempt.
1207    #[test]
1208    fn test_log2_just_below_power_of_two_directed() {
1209        let x = FBig::<mode::HalfEven, 2>::try_from(f64::MAX).unwrap();
1210        // High-precision oracle, then re-rounded to the target precision under each mode.
1211        let oracle = Context::<mode::HalfEven>::new(200)
1212            .log2::<2>(x.repr(), None)
1213            .unwrap()
1214            .value();
1215        for p in [24usize, 40, 53, 64] {
1216            let want_down = Context::<mode::Down>::new(p)
1217                .repr_round_ref(&oracle.repr)
1218                .value();
1219            let want_up = Context::<mode::Up>::new(p)
1220                .repr_round_ref(&oracle.repr)
1221                .value();
1222            let got_down = Context::<mode::Down>::new(p)
1223                .log2::<2>(x.repr(), None)
1224                .unwrap()
1225                .value();
1226            let got_up = Context::<mode::Up>::new(p)
1227                .log2::<2>(x.repr(), None)
1228                .unwrap()
1229                .value();
1230            assert_eq!(got_down.repr(), &want_down, "p={p} Down");
1231            assert_eq!(got_up.repr(), &want_up, "p={p} Up");
1232            // Directed invariant: Up ≥ Down.
1233            assert!(got_up.repr() >= got_down.repr(), "p={p} Up < Down");
1234        }
1235    }
1236
1237    /// Directed `ln` of `x ∈ [1, 2)` must match a high-precision oracle re-rounded under the same
1238    /// mode. This binade (s = 0) is where the radius under-estimated the error: `result` inherits
1239    /// `ln_base`'s over-delivered context, and for `x` just above 1 the scaling even classifies
1240    /// `s = −1`, so `2·sum + s·ln2` cancels and the error stays at `sum`'s magnitude while
1241    /// `result`'s collapses — both make `result.ulp()` the wrong scale for the radius.
1242    fn check_ln_directed_in_unit_binade(k: usize, p: usize) {
1243        // x = (2^k + 1) * 2^-k = 1 + 2^-k, exactly representable at precision p when k < p.
1244        let x = Repr::<2>::new(IBig::from(1i64 << k) + IBig::ONE, -(k as isize));
1245        let oracle = Context::<mode::HalfEven>::new(p + 60)
1246            .ln::<2>(&x, None)
1247            .unwrap()
1248            .value();
1249        let want_down = Context::<mode::Down>::new(p)
1250            .repr_round_ref(&oracle.repr)
1251            .value();
1252        let want_up = Context::<mode::Up>::new(p)
1253            .repr_round_ref(&oracle.repr)
1254            .value();
1255        let got_down = Context::<mode::Down>::new(p)
1256            .ln::<2>(&x, None)
1257            .unwrap()
1258            .value();
1259        let got_up = Context::<mode::Up>::new(p)
1260            .ln::<2>(&x, None)
1261            .unwrap()
1262            .value();
1263        assert_eq!(got_down.repr(), &want_down, "ln(1+2^-{k}) p={p} Down");
1264        assert_eq!(got_up.repr(), &want_up, "ln(1+2^-{k}) p={p} Up");
1265        assert!(got_up.repr() >= got_down.repr(), "ln(1+2^-{k}) p={p} Up < Down");
1266    }
1267
1268    #[test]
1269    fn test_ln_directed_near_one() {
1270        // Sweep the near-1 binade at low precision, including the k close to p cases that
1271        // classify as s = −1 and cancel.
1272        for p in [24usize, 40, 53] {
1273            for k in 1..p.saturating_sub(1) {
1274                check_ln_directed_in_unit_binade(k, p);
1275            }
1276        }
1277    }
1278
1279    /// Fixed inputs for the `log2` oracle differential: moderate magnitudes and the
1280    /// near-boundary regimes the legacy directed-interval implementation was specifically sized for.
1281    fn log2_diff_inputs() -> Vec<Repr<2>> {
1282        let mut v = Vec::new();
1283        for x in [0.5f64, 1.5, 2.0, 3.0, 10.0, 1000.0, 1e-6, 123.456, 2.5e-10] {
1284            v.push(FBig::<mode::HalfEven, 2>::try_from(x).unwrap().into_repr());
1285        }
1286        // Exact powers of two.
1287        for k in [-100isize, -50, -10, -1, 0, 1, 10, 50, 100] {
1288            v.push(Repr::new(IBig::ONE, k));
1289        }
1290        // Just below the largest f64 (log2 ≈ 1024, the directed-regime case in the old comment).
1291        v.push(
1292            FBig::<mode::HalfEven, 2>::try_from(f64::MAX)
1293                .unwrap()
1294                .into_repr(),
1295        );
1296        // The [1, 2) unit binade and its mirror below 1: 1 ± 2^-k and 2 − 2^-k exercise the
1297        // s = −1 cancellation (the second-classified-s-−1 case the doubling compensates).
1298        for k in 1usize..=60 {
1299            v.push(Repr::new(IBig::from(1u64 << k) + IBig::ONE, -(k as isize))); // 1 + 2^-k
1300            v.push(Repr::new(IBig::from((1u64 << k) - 1), -(k as isize))); // 1 − 2^-k
1301            v.push(Repr::new(IBig::from((1u64 << (k + 1)) - 1), -(k as isize)));
1302            // 2 − 2^-k
1303        }
1304        v
1305    }
1306
1307    /// The Ball-based `log2` must round exactly like a high-precision oracle (the definition of
1308    /// correct rounding) across precisions, modes, and the near-boundary inputs.
1309    ///
1310    /// The legacy directed-interval implementation is *not* used as the oracle: it has its own
1311    /// residual 1-ulp bug under directed rounding for `log2(1 − 2^-k)` at p=50 (verified against
1312    /// an independent high-precision computation) — exactly the class of defect this pilot
1313    /// replaces.
1314    fn check_log2_differential<R: ErrorBounds>(p: usize, x: &Repr<2>, oracle: &Repr<2>) {
1315        let ctx = Context::<R>::new(p);
1316        let want = ctx.repr_round_ref(oracle).value();
1317        let got = ctx.log2_internal::<2>(x, None).unwrap().value();
1318        assert_eq!(got.repr, want, "p={p} {} x={x:?}", core::any::type_name::<R>(),);
1319    }
1320
1321    /// Regression: `ln_compute`'s s<0 path (base < 1) must not inflate its radius with a
1322    /// spurious rounding term. An exactly-representable input scaled by a power of two stays
1323    /// exact through the reduction, so the radius must stay small enough that the Ziv loop
1324    /// certifies at the target precision (the old ulp-count code inflated `n` by `B^precision`
1325    /// here and hung `powf` of a base < 1).
1326    #[test]
1327    fn ln_small_base_radius_shrinks_with_guard() {
1328        let ctx = Context::<mode::HalfEven>::new(50);
1329        // 0.2668 (base 10): s = floor(log2(0.2668)) = -2, the s < 0 path.
1330        let x = Repr::<10>::new(IBig::from(2668), -4);
1331        for guard in [4usize, 12, 40, 120] {
1332            let ball = ctx.ln_compute::<10>(&x, 50 + guard, false, None).unwrap();
1333            // The radius must certify at the target precision: rad ≤ 1 ulp of the midpoint at
1334            // precision 50 (i.e. the error interval fits a rounding preimage on this attempt).
1335            assert!(
1336                ball.rad <= ulp_mag::<10>(&ball.mid, 50),
1337                "radius {:?} at guard={guard} does not certify (the s<0 reduction inflated it)",
1338                ball.rad
1339            );
1340        }
1341    }
1342
1343    #[test]
1344    fn log2_ball_matches_oracle() {
1345        let inputs = log2_diff_inputs();
1346        // Moderate precisions: full input sweep, all five modes.
1347        for p in [20usize, 50, 100] {
1348            for x in &inputs {
1349                // The oracle is mode-independent: a high-precision HalfEven value re-rounded
1350                // under each target mode.
1351                let oracle = Context::<mode::HalfEven>::new(p + 60)
1352                    .log2::<2>(x, None)
1353                    .unwrap()
1354                    .value();
1355                check_log2_differential::<mode::HalfEven>(p, x, &oracle.repr);
1356                check_log2_differential::<mode::Down>(p, x, &oracle.repr);
1357                check_log2_differential::<mode::Up>(p, x, &oracle.repr);
1358                check_log2_differential::<mode::Zero>(p, x, &oracle.repr);
1359                check_log2_differential::<mode::Away>(p, x, &oracle.repr);
1360            }
1361        }
1362        // The arbitrary-precision regime: a reduced sweep (directed modes still exercised).
1363        for x in inputs.iter().step_by(9) {
1364            let oracle = Context::<mode::HalfEven>::new(560)
1365                .log2::<2>(x, None)
1366                .unwrap()
1367                .value();
1368            check_log2_differential::<mode::HalfEven>(500, x, &oracle.repr);
1369            check_log2_differential::<mode::Down>(500, x, &oracle.repr);
1370            check_log2_differential::<mode::Up>(500, x, &oracle.repr);
1371        }
1372    }
1373
1374    #[test]
1375    fn ln_1p_ball_bounds_negative_arg() {
1376        // Regression: `ln_1p_ball`'s input-error adjust dropped the precision-difference term
1377        // (−p_arg+p_ln). For an arg with 1+arg ∈ (0, 1) (e.g. atanh(x<0) near the pole),
1378        // `ln_compute` doubles the work precision (the s<0 path), so the radius must still
1379        // cover the true value after the derivative fold.
1380        type F = FBig<mode::HalfEven, 10>;
1381        let ctx = Context::<mode::HalfEven>::new(10);
1382        // arg mid = −0.9999 at precision 10 (ulp = 1e-10), rad = 5 ulps ⇒ true arg = −0.9999000005.
1383        let mid = F::from_parts(IBig::from(-9999000000i64), -10)
1384            .with_precision(10)
1385            .value();
1386        let rad = ulps::<10>(&mid.repr, 10, 5);
1387        let arg = Ball::<10>::with_error(mid.into_repr(), rad);
1388        let ln_ball = ctx.ln_1p_ball::<10>(&arg, None).unwrap();
1389        // true ln(1+arg) = ln(1 − 0.9999000005) = ln(9.99995e-5), oracle at precision 60.
1390        let one_plus_true = F::from_parts(IBig::from(999995i64), -10)
1391            .with_precision(0)
1392            .value();
1393        let true_ln = one_plus_true
1394            .with_precision(60)
1395            .value()
1396            .ln()
1397            .with_precision(0)
1398            .value();
1399        let mid_f = F::new(ln_ball.mid.clone(), Context::<mode::HalfEven>::new(0));
1400        let diff = (mid_f - true_ln).abs();
1401        let bound = F::new(ln_ball.rad.to_repr::<10>(), Context::<mode::HalfEven>::new(0));
1402        assert!(
1403            diff.abs_cmp(&bound).is_le(),
1404            "ln_1p_ball: |mid − true| = {diff} > rad = {bound}"
1405        );
1406    }
1407
1408    /// Regression (issue #103): `ln` of a huge-exponent argument must reduce as
1409    /// `ln(m) + k·ln(B)` — the reduction may never materialize a power of the exponent *gap*
1410    /// (`ln(1e1000000000)` previously ballooned to gigabytes of integer and never returned).
1411    #[test]
1412    fn ln_huge_exponent_arguments() {
1413        use alloc::string::ToString;
1414        let ctx = Context::<mode::HalfEven>::new(34);
1415        // ln(10^1000000000) = 10^9·ln(10), correctly rounded to 34 digits
1416        let x = Repr::<10>::new(IBig::ONE, 1_000_000_000);
1417        let r = ctx.ln::<10>(&x, None).unwrap().value();
1418        assert_eq!(r.to_string(), "2302585092.994045684017991454684364");
1419        // ln(10^-1000000000) = −ln(10^1000000000) — the s<0 path (double work precision)
1420        let x = Repr::<10>::new(IBig::ONE, -1_000_000_000);
1421        let r = ctx.ln::<10>(&x, None).unwrap().value();
1422        assert_eq!(r.to_string(), "-2302585092.994045684017991454684364");
1423        // a mid-size exponent on the same path (the previously-working regime)
1424        let x = Repr::<10>::new(IBig::ONE, 1_000_000);
1425        let r = ctx.ln::<10>(&x, None).unwrap().value();
1426        assert_eq!(r.to_string(), "2302585.092994045684017991454684364");
1427    }
1428
1429    /// Huge-exponent arguments under directed modes must match a high-precision oracle
1430    /// re-rounded under the same mode — exercising both reduction stages (the base-exponent
1431    /// re-tag and the bounded power-of-two finish) on bases 2 and 10, with exact and inexact
1432    /// significands.
1433    #[test]
1434    fn ln_directed_matches_oracle_huge_exponents() {
1435        fn check<const B: Word, R: ErrorBounds>(p: usize, x: &Repr<B>) {
1436            let oracle = Context::<mode::HalfEven>::new(p + 60)
1437                .ln::<B>(x, None)
1438                .unwrap()
1439                .value();
1440            let want = Context::<R>::new(p).repr_round_ref(&oracle.repr).value();
1441            let got = Context::<R>::new(p).ln::<B>(x, None).unwrap().value();
1442            assert_eq!(got.repr, want, "p={p} {} x={x:?} base={B}", core::any::type_name::<R>());
1443        }
1444        let inputs10 = [
1445            Repr::<10>::new(IBig::ONE, 1_000_000_000),
1446            Repr::<10>::new(IBig::from(3), 999_999_998), // 3e999999998
1447            Repr::<10>::new(IBig::ONE, -1_000_000_000),
1448            Repr::<10>::new(IBig::from(7), -1_000_000_000), // 7e-1000000000
1449            Repr::<10>::new(IBig::from(12345), -9_999_995), // 1.2345e-9999991
1450        ];
1451        for p in [20usize, 34, 50, 100] {
1452            for x in &inputs10 {
1453                check::<10, mode::HalfEven>(p, x);
1454                check::<10, mode::Down>(p, x);
1455                check::<10, mode::Up>(p, x);
1456                check::<10, mode::Zero>(p, x);
1457            }
1458        }
1459        let inputs2 = [
1460            Repr::<2>::new(IBig::ONE, 1 << 30),
1461            Repr::<2>::new(IBig::from(3), -(1 << 30)),
1462            Repr::<2>::new(IBig::ONE, -(1 << 20)),
1463        ];
1464        for p in [20usize, 50, 100, 500] {
1465            for x in &inputs2 {
1466                check::<2, mode::HalfEven>(p, x);
1467                check::<2, mode::Down>(p, x);
1468                check::<2, mode::Up>(p, x);
1469                check::<2, mode::Zero>(p, x);
1470            }
1471        }
1472    }
1473
1474    // The reconstruction constants' 8-ulp radii are amplified by the base-power scale factor
1475    // (`100·rad(ln 10)` here), which used to sit at a third of the target half-ulp at 6 digits —
1476    // one Ziv retry whenever the work value also landed near the rounding boundary. The constants
1477    // now carry enough extra digits to keep the amplified radius sub-ulp (see `ln_compute`).
1478    #[cfg(feature = "std")]
1479    #[test]
1480    fn ln_large_base_power_certifies_first_attempt() {
1481        let ctx = Context::<mode::HalfEven>::new(6);
1482        crate::ziv_retries_reset();
1483        let _ = ctx.ln::<10>(&Repr::<10>::new(1.into(), 100), None).unwrap();
1484        assert_eq!(
1485            crate::ziv_retries(),
1486            0,
1487            "DBig ln(1e100) @6 should certify on the first attempt"
1488        );
1489    }
1490
1491    /// `ln_1p` compares its `−1` boundary *exactly*: `−1 + 10⁻ⁿ` is the finite `ln(10⁻ⁿ)`, not `−∞`.
1492    ///
1493    /// Regression: `ln_compute` was handed the `1 + x` cancellation with a *zero* midpoint, where
1494    /// `log2_bounds` saturates to `−∞` — the reduction panicked on the cast (`attempt to subtract
1495    /// with overflow`, wrapping to a nonsense exponent in release). The zero now reports the
1496    /// whole-line radius, so the loop retries at a guard where the argument is representable.
1497    #[test]
1498    fn test_ln_1p_endpoint_compare_exactly() {
1499        for &p in &[20usize, 50, 100, 500] {
1500            let n = 3 * p / 2 + 5;
1501            // −1 + 10^-n = −(10^n − 1)·10^-n
1502            let x = Repr::<10>::new(-(IBig::from(10).pow(n) - IBig::from(1)), -(n as isize));
1503            let ctx = Context::<mode::HalfEven>::new(p);
1504            let v = ctx.ln_1p::<10>(&x, None).unwrap().value();
1505            assert!(v.repr().is_finite(), "ln_1p(-1 + 10^-{n}) at p={p} is finite, not -inf");
1506            let hi = Context::<mode::HalfEven>::new(p + 60)
1507                .ln_1p::<10>(&x, None)
1508                .unwrap()
1509                .value();
1510            assert_eq!(v, hi.with_precision(p).value(), "ln_1p(-1 + 10^-{n}) at p={p}");
1511            // exactly −1 is still the pole
1512            let at_pole = ctx
1513                .ln_1p::<10>(&Repr::<10>::neg_one(), None)
1514                .unwrap()
1515                .value();
1516            assert!(at_pole.repr().is_infinite() && at_pole.repr().sign() == Sign::Negative);
1517        }
1518    }
1519}