Skip to main content

dashu_float/
exp.rs

1use core::cmp::Ordering;
2use core::convert::TryInto;
3
4use crate::{
5    ball::Ball,
6    error::{assert_finite, assert_limited_precision, FpError, FpResult},
7    fbig::FBig,
8    math::cache::{reborrow_cache, ConstCache},
9    repr::{Context, Repr, Word},
10    round::{mode, ErrorBounds, Round, Rounded, Rounding::*},
11};
12use dashu_base::{Abs, AbsOrd, Approximation::*, BitTest, DivRemEuclid, EstimatedLog2, Sign};
13use dashu_int::{IBig, UBig};
14
15// `|x|` (in log2) above which exp's reduction quotient `s = floor(x/ln B)` might overflow `isize`,
16// so the hoisted overflow probe must run instead of the fast skip. `s` overflows when
17// `|x| > isize::MAX · ln B`, i.e. `log2|x| > log2(isize::MAX) + log2(ln B)`; the minimum (over
18// `B ≥ 2`) is `~isize::BITS − 1.5`, and the `−3` margin stays below it. The literal was `61` (the
19// 64-bit value); on 32-bit `isize` it must be ~29 or `exp_compute`'s `s.try_into().expect()` panics
20// for inputs like `exp(-2⁵⁰)` (whose `s ≈ -1.8e15` overflows 32-bit `isize`).
21const EXP_OVERFLOW_PROBE_LOG2: f32 = (isize::BITS - 3) as f32;
22
23// Maximum bit length of a `powi` exponent for which the binary squaring chain is feasible. The
24// chain runs `n.bit_len() - 1` correctly-rounded squarings at a working precision that grows with
25// `n.bit_len()`, and compounds relative error ~`2^nlen · ulp`; for `nlen` in the millions/billions
26// (an `IBig` exponent far past `i64`) a single attempt exhausts memory. Exponents beyond `i64`
27// (`nlen > 64`) only ever produce a finite result when `|base| ≈ 1` (otherwise the magnitude
28// overflows the finite range), so they go to the `exp(y·ln x)` fallback instead of the chain.
29const MAX_POWI_CHAIN_BITS: usize = 64;
30
31// Margin certifying a `powi` result is outside the finite range: the magnitude guard estimates the
32// result's log2 as `e · log2(base)` in f64, where `e` is up to `i64::MAX` and `isize::MAX as f64`
33// is itself rounded — together ~2^17 of f64 error near the boundary. A result whose log2 is within
34// this margin of the threshold is deferred to the squaring chain (whose `checked_mul` catches a
35// genuine overflow) rather than risk a false-positive overflow returning ±∞.
36const POWI_RANGE_MARGIN: f64 = (1 << 20) as f64;
37
38// `powi` (integer power), `powf`/`exp`/`exp_m1` route through Ziv-backed Context methods, which
39// require `R: ErrorBounds` for their correctness guarantee.
40impl<R: ErrorBounds, const B: Word> FBig<R, B> {
41    /// Raise the floating point number to an integer power.
42    ///
43    /// # Examples
44    ///
45    /// ```
46    /// # use dashu_base::ParseError;
47    /// # use dashu_float::DBig;
48    /// # use core::str::FromStr;
49    /// let a = DBig::from_str("-1.234")?;
50    /// assert_eq!(a.powi(10.into()), DBig::from_str("8.188")?);
51    /// # Ok::<(), ParseError>(())
52    /// ```
53    #[inline]
54    pub fn powi(&self, exp: IBig) -> FBig<R, B> {
55        self.context.unwrap_fp(self.context.powi(&self.repr, exp))
56    }
57
58    /// Raise the floating point number to an floating point power.
59    ///
60    /// # Examples
61    ///
62    /// ```
63    /// # use dashu_base::ParseError;
64    /// # use dashu_float::DBig;
65    /// # use core::str::FromStr;
66    /// let x = DBig::from_str("1.23")?;
67    /// let y = DBig::from_str("-4.56")?;
68    /// assert_eq!(x.powf(&y), DBig::from_str("0.389")?);
69    /// # Ok::<(), ParseError>(())
70    /// ```
71    #[inline]
72    pub fn powf(&self, exp: &Self) -> Self {
73        let context = Context::max(self.context, exp.context);
74        context.unwrap_fp(context.powf(&self.repr, &exp.repr, None))
75    }
76
77    /// Calculate the exponential function (`eˣ`) on the floating point number.
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// # use dashu_base::ParseError;
83    /// # use dashu_float::DBig;
84    /// # use core::str::FromStr;
85    /// let a = DBig::from_str("-1.234")?;
86    /// assert_eq!(a.exp(), DBig::from_str("0.2911")?);
87    /// # Ok::<(), ParseError>(())
88    /// ```
89    #[inline]
90    pub fn exp(&self) -> FBig<R, B> {
91        self.context.unwrap_fp(self.context.exp(&self.repr, None))
92    }
93
94    /// Calculate the exponential minus one function (`eˣ-1`) on the floating point number.
95    ///
96    /// # Examples
97    ///
98    /// ```
99    /// # use dashu_base::ParseError;
100    /// # use dashu_float::DBig;
101    /// # use core::str::FromStr;
102    /// let a = DBig::from_str("-0.1234")?;
103    /// assert_eq!(a.exp_m1(), DBig::from_str("-0.11609")?);
104    /// # Ok::<(), ParseError>(())
105    /// ```
106    #[inline]
107    pub fn exp_m1(&self) -> FBig<R, B> {
108        self.context
109            .unwrap_fp(self.context.exp_m1(&self.repr, None))
110    }
111}
112
113impl<R: Round> Context<R> {
114    /// Near-correct exp core: evaluate `exp(x)` (or `exp_m1(x)` when `minus_one`) at
115    /// `work_precision`, returning a [`Ball`] whose radius is derived mechanically.
116    ///
117    /// Shared by the Ziv-backed `exp`/`exp_m1` (which retry it) and usable directly where only a
118    /// near-correct value is needed. The caller must have pre-checked that the reduction quotient
119    /// `s = floor(x/ln B)` fits `isize` (astronomical `|x|` overflows and is handled before the
120    /// Ziv loop, since this closure can't return `Err`). `n` (the reduction power, `≈ √p`) is
121    /// derived from the *target* precision and is constant across retries.
122    pub(crate) fn exp_compute<const B: Word>(
123        &self,
124        x: &Repr<B>,
125        work_precision: usize,
126        minus_one: bool,
127        n: usize,
128        mut cache: Option<&mut ConstCache>,
129    ) -> Result<Ball<B>, FpError> {
130        // exp(x) = B^s · exp(r)^(Bⁿ), with r = x − s·ln(B) reduced so |r| < B⁻ⁿ.
131        let context = Context::<mode::HalfEven>::new(work_precision);
132        let x_ball = Ball::from_rounded(context.repr_round_ref(x).map(|r| FBig::new(r, context)));
133
134        // When minus_one is true and |x| < 1/B, evaluate the Maclaurin series without scaling
135        // (no Bⁿ reduction, no powering — n_eff = 0).
136        let no_scaling = minus_one && x_ball.mid.log2_est() < -B.log2_est();
137        let (s, r_ball, n_eff) = if no_scaling {
138            (0isize, x_ball, 0usize)
139        } else {
140            // The reduction quotient `s = floor(x / ln B)` amplifies ln(B)'s rounding error by
141            // |x|: a 1-ulp error in ln(B) shifts `s` (and thus the result exponent) by ~|x|/ln B.
142            // For large |x| the work precision is far too low to pin `s` — exp(5.7e14) at p=24 was
143            // certified with the exponent off by ~1000 — so compute ln(B) with `⌈log_B|x|⌉ + 2`
144            // extra digits, enough that the reduction contributes well under one work-ulp and the
145            // series/powering radius bounds the total error. (The bounds, not the point estimate,
146            // guard the inflation magnitude.)
147            let x_log2_ub = x_ball.mid.log2_bounds().1;
148            let extra = if x_log2_ub > 0.0 {
149                (x_log2_ub / B.log2_est()) as usize + 2
150            } else {
151                2
152            };
153            // ln(B) as a ball: the cached bases (2, 10, powers of 2) are correctly rounded (the
154            // fixed 8-ulp bound in `ln_base_ball` is sound there), while generic bases carry
155            // `ln_compute`'s mechanical radius (their atanh series error can be far larger than 8).
156            let logb_ball = Context::<mode::HalfEven>::new(work_precision + extra)
157                .ln_base_ball::<B>(reborrow_cache(&mut cache));
158            let x_sign = x_ball.mid.repr().sign();
159            let (s_big, _) = x_ball.mid.clone().div_rem_euclid(logb_ball.mid.clone());
160            let s: isize = match s_big.try_into() {
161                Ok(s) => s,
162                Err(_) => {
163                    // |x| is astronomical — the reduction quotient overflows isize. The magnitude
164                    // gate in `exp_internal` is meant to catch this first; reaching here is a
165                    // gray-zone miss, so surface the range error it would have returned.
166                    return Err(if x_sign == Sign::Positive {
167                        FpError::Overflow(Sign::Positive)
168                    } else {
169                        FpError::Underflow(Sign::Positive)
170                    });
171                }
172            };
173            // r = x − s·ln(B), as a ball: the cancellation and ln(B)'s error are tracked by the
174            // Ball propagation.
175            let r_ball = x_ball.sub(&logb_ball.scale_int(&IBig::from(s)));
176            (s, r_ball, n)
177        };
178        let r_ball = r_ball.shift(n_eff as isize);
179
180        // Maclaurin series: exp(r) = 1 + Σ rⁱ/i! (exp_m1(x) = Σ xⁱ/i! when no_scaling).
181        let one = Ball::exact_int(r_ball.mid.precision(), IBig::ONE);
182        let mut factorial = IBig::ONE;
183        let mut pow = r_ball.clone();
184        let mut sum = if no_scaling {
185            r_ball.clone()
186        } else {
187            one.add(&r_ball)
188        };
189        let mut k = 2u32;
190        loop {
191            factorial *= k;
192            pow = pow.mul(&r_ball);
193
194            let increase = pow.div_exact(&factorial);
195            if increase.mid.abs_cmp(&sum.mid.ulp_lb()).is_le() {
196                break;
197            }
198            sum = sum.add(&increase);
199            k += 1;
200        }
201
202        // Omitted series tail: the exp terms shrink by r/(i+1) < 1/4 (|r| < B⁻ⁿ), so the tail is
203        // < 2 ulps of sum.
204        sum.inflate(&IBig::from(2));
205
206        if no_scaling {
207            // exp_m1(x) = sum directly.
208            return Ok(sum);
209        }
210        // Powering: exp(r)^(Bⁿ) = sum^Bⁿ, via binary exponentiation (the compounding rounding is
211        // tracked by the Ball multiplications). The chain is exact only when the series sum is
212        // exact (impossible here — sum ≈ exp(r) with |r| < B⁻ⁿ), so the exact flag is ignored.
213        let bn = Repr::<B>::BASE.pow(n);
214        let (v_ball, _) = sum.pow_exact(&bn)?;
215
216        // B^s is an exact power-of-base shift: `exp(x) = B^s · exp(r)^(Bⁿ)`.
217        let v_shifted = v_ball.shift(-s);
218
219        if minus_one {
220            // exp_m1(x) = exp(x) − 1; the subtraction folds one rounding ulp.
221            let one = Ball::exact_int(v_shifted.mid.precision(), IBig::ONE);
222            Ok(v_shifted.sub(&one))
223        } else {
224            Ok(v_shifted)
225        }
226    }
227
228    /// `exp` of a *ball* input. [`exp_compute`](Self::exp_compute) evaluates on `x.mid`; the input
229    /// ball's own error `|θ| ≤ x.n·ulp(x)` then contributes `exp(x)·|θ|` to the result (the
230    /// exponential's derivative is itself), folded into the radius.
231    pub(crate) fn exp_ball<const B: Word>(
232        &self,
233        x: &Ball<B>,
234        mut cache: Option<&mut ConstCache>,
235    ) -> Result<Ball<B>, FpError> {
236        let n = 1usize << (self.precision.bit_len() / 2);
237        let mut result = self.exp_compute::<B>(
238            x.mid.repr(),
239            x.mid.precision(),
240            false,
241            n,
242            reborrow_cache(&mut cache),
243        )?;
244        if !x.n.is_zero() {
245            // `e_r` is the raw significand exponent (`mid_r = sig_r·B^(e_r)`), `lead_*` is the
246            // leading position (`lead_exp`), so `ulp_x = B^(lead_x − p_x)` and
247            // `ulp_r = B^(lead_r − p_r)`. The exponential's derivative is itself, so the input
248            // error propagates as `n_x·ulp_x·|exp|/ulp_r = n_x·sig_r·B^(lead_x − p_x + e_r − lead_r + p_r)`,
249            // the exact derivative bound (no small-constant factor needed, unlike ln's 1/(1+x)).
250            // The `sig_r = |mid_r|` factor is essential: it scales the input ulp up to the
251            // result's magnitude. Omitting it under-bounds the radius by `sig_r` (≈ B^(p−1)) — the
252            // Ziv containment test then certifies an interval that does not contain the true value
253            // (e.g. `powf` with a large |y·ln x|).
254            let sig_r = result.mid.repr().significand.clone().abs();
255            let e_r = result.mid.repr().exponent;
256            let lead_r = Ball::lead_exp(&result.mid);
257            let p_r = result.mid.precision();
258            let lead_x = Ball::lead_exp(&x.mid);
259            let p_x = x.mid.precision();
260            let shift = lead_x - p_x as isize + e_r - lead_r + p_r as isize;
261            result.inflate(&crate::ball::ceil_shift::<B>(x.n.clone() * sig_r, shift));
262        }
263        Ok(result)
264    }
265
266    /// `exp` of a *ball* input. [`exp_compute`](Self::exp_compute) evaluates on `x.mid`; the input
267    /// ball's own error `|θ| ≤ x.n·ulp(x)` then contributes `exp(x)·|θ|` to the result (the
268    /// exponential's derivative is itself), folded into the radius.
269    /// Directed saturation endpoint for an FBig result that has underflowed below the smallest
270    /// representable magnitude (its exponent would fall below `isize::MIN`). Outward modes round
271    /// the magnitude up to the smallest `B^{isize::MIN}` of the result's sign; toward-zero, the
272    /// opposite direction, and nearest round to signed zero. This mirrors the f32/f64 directed
273    /// underflow and is the shared endpoint used by `exp_extreme_negative`, `powi`, and `powf`, so
274    /// a directed `pow` (e.g. `pow(10, y)` ≈ `exp(y·ln 10)`) saturates to the same value `exp` does
275    /// — keeping `Up ≥ Down` consistent across them. The endpoint carries the input context, so a
276    /// downstream op keeps a limited precision.
277    pub(crate) fn underflow_repr_endpoint<const B: Word>(&self, sign: Sign) -> Rounded<FBig<R, B>> {
278        let adj = if sign == Sign::Positive {
279            R::round_low_part(&IBig::ZERO, Sign::Positive, || Ordering::Less)
280        } else {
281            R::round_low_part(&IBig::ZERO, Sign::Negative, || Ordering::Less)
282        };
283        match adj {
284            AddOne => Inexact(FBig::new(Repr::new(IBig::ONE, isize::MIN), *self), AddOne),
285            SubOne => Inexact(
286                FBig::new(
287                    Repr::new(IBig::from_parts(Sign::Negative, UBig::ONE), isize::MIN),
288                    *self,
289                ),
290                SubOne,
291            ),
292            _ => Inexact(FBig::new(Repr::<B>::zero_with_sign(sign), *self), NoOp),
293        }
294    }
295
296    /// Directed saturation endpoint for an FBig result that has overflowed above the largest
297    /// representable magnitude (its exponent would exceed `isize::MAX`). Outward modes (Up/Away for
298    /// positive, Down/Away for negative) and nearest reach `±∞`; inward modes (toward-zero, and the
299    /// opposite-infinity direction) saturate to the largest finite `(Bᵖ−1) × B^{isize::MAX}` — the
300    /// all-`(B−1)` significand at the max exponent, mirroring MPFR's `mpfr_setmax` (which fills the
301    /// significand with all 1-bits *at the output precision* — the significand is `p` digits, not
302    /// the value's magnitude). The largest finite is ill-defined at unlimited precision, so this
303    /// panics when `precision == 0`.
304    pub(crate) fn overflow_repr_endpoint<const B: Word>(&self, sign: Sign) -> Rounded<FBig<R, B>> {
305        assert_limited_precision(self.precision);
306        let adj = if sign == Sign::Positive {
307            R::round_low_part(&IBig::ONE, Sign::Positive, || Ordering::Greater)
308        } else {
309            R::round_low_part(&IBig::NEG_ONE, Sign::Negative, || Ordering::Greater)
310        };
311        match adj {
312            AddOne => Inexact(FBig::new(Repr::infinity_with_sign(Sign::Positive), *self), AddOne),
313            SubOne => Inexact(FBig::new(Repr::infinity_with_sign(Sign::Negative), *self), SubOne),
314            _ => {
315                // Largest finite at this precision: (B^p − 1) × B^{isize::MAX}.
316                let max_mag = Repr::<B>::BASE.pow(self.precision) - UBig::ONE;
317                Inexact(
318                    FBig::new(Repr::new(IBig::from_parts(sign, max_mag), isize::MAX), *self),
319                    NoOp,
320                )
321            }
322        }
323    }
324}
325
326// `powi` (integer power), `powf` (non-integer exponent), `exp`, and `exp_m1` are correctly rounded
327// via the Ziv loop, so they require `R: ErrorBounds`. `powf` with an integer-valued exponent
328// delegates to `powi`.
329impl<R: ErrorBounds> Context<R> {
330    /// Raise the floating point number to an integer power under this context, correctly rounded
331    /// via a Ziv retry loop.
332    ///
333    /// `base^n` is computed by left-to-right binary exponentiation (repeated squaring); a negative
334    /// exponent computes `(1/base)^|n|`, so the sign-dependent overflow/underflow falls out
335    /// naturally. Each squaring compounds the relative error (it roughly doubles per step), so
336    /// after `n.bit_len()` squarings the error is bounded by about `2^nlen · ulp` — the Ziv radius
337    /// reflects that, and the loop retries with more guard digits until the working-precision
338    /// interval unambiguously determines the target rounding.
339    ///
340    /// # Examples
341    ///
342    /// ```
343    /// # use dashu_base::ParseError;
344    /// # use dashu_float::DBig;
345    /// # use core::str::FromStr;
346    /// use dashu_base::Approximation::*;
347    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
348    ///
349    /// let context = Context::<HalfAway>::new(2);
350    /// let a = DBig::from_str("-1.234")?;
351    /// assert_eq!(context.powi(&a.repr(), 10.into()), Ok(Inexact(DBig::from_str("8.2")?, AddOne)));
352    /// # Ok::<(), ParseError>(())
353    /// ```
354    ///
355    /// # Panics
356    ///
357    /// Panics if the precision is unlimited and the exponent is negative (the exact `1/base` is
358    /// not finite in general).
359    pub fn powi<const B: Word>(&self, base: &Repr<B>, exp: IBig) -> FpResult<FBig<R, B>> {
360        if base.is_infinite() {
361            return Err(FpError::InfiniteInput);
362        }
363        let (exp_sign, n) = exp.into_parts();
364        let negative = exp_sign == Sign::Negative;
365        if negative {
366            // a negative exponent needs 1/base, which is not finite at unlimited precision
367            assert_limited_precision(self.precision);
368        }
369
370        if n.is_zero() {
371            return Ok(Exact(FBig::ONE));
372        }
373        if n.is_one() {
374            if negative {
375                // base^(-1) = 1/base: a single correctly-rounded division
376                return self.div(&Repr::one(), base);
377            }
378            let repr = self.repr_round_ref(base);
379            return Ok(repr.map(|v| FBig::new(v, *self)));
380        }
381
382        // Zero base (±0): a positive exponent gives ±0, a negative one ±inf; the sign follows |n|'s
383        // parity. Short-circuit before the magnitude pre-check (whose log2 estimate is meaningless
384        // for zero) and before the squaring chain (which can't start from zero).
385        let odd = n.bit(0);
386        if base.significand.is_zero() {
387            let neg_sign = base.sign() == Sign::Negative && odd;
388            if negative {
389                let sign = if neg_sign {
390                    Sign::Negative
391                } else {
392                    Sign::Positive
393                };
394                return Ok(Exact(FBig::new(Repr::<B>::infinity_with_sign(sign), *self)));
395            }
396            let repr = if neg_sign {
397                Repr::<B>::neg_zero()
398            } else {
399                Repr::<B>::zero()
400            };
401            return Ok(Exact(FBig::new(repr, *self)));
402        }
403
404        // Magnitude pre-check: the result's log2 is `signed_exp · log2|base|`; outside the finite
405        // exponent range it short-circuits to overflow/underflow instead of letting the squaring
406        // chain overflow mid-computation (the Ziv closure below can't return `Err`).
407        //
408        // Use the *bounds* of log2(base), never the point estimate `log2_est`: when base is very
409        // close to 1 (a large significand with a large negative exponent), log2(base) is the
410        // difference of two large terms and suffers catastrophic cancellation — `log2_est` returns
411        // ~1e-4 of f32 noise rather than ~0. Scaled by a large exponent that noise crosses the
412        // overflow threshold, which on 32-bit is only isize::MAX·log2(B) ≈ 7e9 (vs ≈3e19 on 64-bit),
413        // so the guard fires spuriously and returns ±inf — see issue #95 (it crashed high-precision
414        // `FBig::with_base` on wasm32/i686). The bounds are derived from the exact significand bit
415        // length, so they don't cancel. Declare an extreme result only when a bound certifies it
416        // (no false positives); anything ambiguous is computed.
417        let (base_log2_lb, base_log2_ub) = base.log2_bounds();
418        let base_log2_lb = base_log2_lb as f64;
419        let base_log2_ub = base_log2_ub as f64;
420        let threshold = (isize::MAX as f64) * (B.log2_est() as f64);
421        let threshold_certain = threshold + POWI_RANGE_MARGIN;
422        let exp_f64 = i64::try_from(&n).ok().map(|e| e as f64);
423        // `lb_side` certifies |base| > 1 by a wide margin; `ub_side` certifies |base| < 1. (For the
424        // None case |exp| is unbounded, so the bound's sign alone decides.) A negative exponent
425        // swaps which side over- vs underflows.
426        let lb_side = match exp_f64 {
427            Some(e) => e * base_log2_lb > threshold_certain,
428            None => base_log2_lb > 0.0,
429        };
430        let ub_side = match exp_f64 {
431            Some(e) => e * base_log2_ub < -threshold_certain,
432            None => base_log2_ub < 0.0,
433        };
434        if lb_side || ub_side {
435            // |base|>1 (lb_side): positive exp → overflow, negative exp → underflow.
436            // |base|<1 (ub_side): positive exp → underflow, negative exp → overflow.
437            let overflow = (lb_side && !negative) || (ub_side && negative);
438            let sign = if base.sign() == Sign::Negative && odd {
439                Sign::Negative
440            } else {
441                Sign::Positive
442            };
443            return Err(if overflow {
444                FpError::Overflow(sign)
445            } else {
446                FpError::Underflow(sign)
447            });
448        }
449
450        // |exp| doesn't fit i64 and the bounds straddle 0, so |base| is within the bounds of 1.
451        // If it is *exactly* ±1 the result is ±1 for any exponent (short-circuit so the squaring
452        // chain doesn't iterate over exp's enormous bit length); otherwise |base| ≈ 1 but ≠ 1, the
453        // huge power is still finite, and we fall through to compute it.
454        if exp_f64.is_none() && base.significand.is_one() && base.exponent == 0 {
455            let repr = if base.sign() == Sign::Negative && odd {
456                Repr::<B>::neg_one()
457            } else {
458                Repr::<B>::one()
459            };
460            return Ok(Exact(FBig::new(repr, *self)));
461        }
462
463        let nlen = n.bit_len();
464        // Exponents past `i64` make the squaring chain infeasible (`nlen - 1` squarings at a
465        // working precision that grows with `nlen`) and, when the magnitude also overflows the
466        // finite range, can drive it to exhaust memory. The only finite results at that scale have
467        // `|base| ≈ 1`, which the `exp(y·ln x)` fallback computes without scaling the working
468        // precision with `nlen` — so it cannot allocate unboundedly.
469        if nlen > MAX_POWI_CHAIN_BITS {
470            let signed_exp = IBig::from_parts(exp_sign, n.clone());
471            return self.powi_via_exp_log(base, &signed_exp);
472        }
473        // The chain runs on the magnitude `start` (base, or its reciprocal for a negative
474        // exponent), whose range-error sign is the *intermediate* sign; remap any overflow/underflow
475        // to the true result sign (base sign × exponent parity) so the directed endpoint is correct.
476        let result_sign = if base.sign() == Sign::Negative && odd {
477            Sign::Negative
478        } else {
479            Sign::Positive
480        };
481        let initial_guard = nlen + self.base_guard_digits::<B>() + 2;
482        self.ziv(initial_guard, |guard| {
483            let pw = self.precision + guard;
484            let work = Context::<mode::HalfEven>::new(pw);
485            // start from base (positive exponent, always exact) or its working-precision reciprocal
486            // (negative exponent, exact only when 1/base is exactly representable). A range error
487            // here (e.g. 1/base underflows for an extreme base) is remapped to the result sign and
488            // propagated — saturating would feed the chain an infinity or zero it can't recover from.
489            let start_ball = if negative {
490                match work.div(&Repr::one(), base) {
491                    Ok(v) => Ball::from_rounded(v),
492                    Err(FpError::Overflow(_)) => return Err(FpError::Overflow(result_sign)),
493                    Err(FpError::Underflow(_)) => return Err(FpError::Underflow(result_sign)),
494                    Err(e) => return Err(e),
495                }
496            } else {
497                Ball::exact(FBig::new(base.clone(), work))
498            };
499            // The squaring chain compounds the error mechanically; when the whole chain is exact
500            // (exact start, no rounding anywhere) the Ball's n is 0, reporting a zero radius — the
501            // exactly-representable directed-rounding case that no nonzero radius could certify.
502            let (res_ball, _) = start_ball.pow_exact(&n).map_err(|e| match e {
503                FpError::Overflow(_) => FpError::Overflow(result_sign),
504                FpError::Underflow(_) => FpError::Underflow(result_sign),
505                other => other,
506            })?;
507            Ok(res_ball.to_value_radius::<R>())
508        })
509    }
510
511    /// Raise the floating point number to an floating point power under this context.
512    ///
513    /// A non-integer exponent is correctly rounded via a Ziv loop. An integer-valued exponent
514    /// delegates to [`powi`](Context::powi) (binary exponentiation), which also accepts a negative
515    /// base — its sign is fixed by the exponent's parity — so `pow(-x, n)` is in domain here for
516    /// integer `n`. Both paths are correctly rounded.
517    ///
518    /// # Examples
519    ///
520    /// ```
521    /// # use dashu_base::ParseError;
522    /// # use dashu_float::DBig;
523    /// # use core::str::FromStr;
524    /// use dashu_base::Approximation::*;
525    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
526    ///
527    /// let context = Context::<HalfAway>::new(2);
528    /// let x = DBig::from_str("1.23")?;
529    /// let y = DBig::from_str("-4.56")?;
530    /// assert_eq!(context.powf(&x.repr(), &y.repr(), None), Ok(Inexact(DBig::from_str("0.39")?, AddOne)));
531    /// # Ok::<(), ParseError>(())
532    /// ```
533    ///
534    /// # Panics
535    ///
536    /// Panics if the precision is unlimited.
537    pub fn powf<const B: Word>(
538        &self,
539        base: &Repr<B>,
540        exp: &Repr<B>,
541        cache: Option<&mut ConstCache>,
542    ) -> FpResult<FBig<R, B>> {
543        if base.is_infinite() || exp.is_infinite() {
544            return Err(FpError::InfiniteInput);
545        }
546        assert_limited_precision(self.precision);
547
548        // shortcuts
549        if exp.is_pos_zero() || exp.is_neg_zero() {
550            // pow(x, ±0) = 1 for any base (IEEE 754 §9.2.1); `-0` is numerically zero, so it
551            // must take the same shortcut as `+0` (otherwise a negative base falls through to
552            // the OutOfDomain path below).
553            return Ok(Exact(FBig::ONE));
554        } else if exp.is_one() {
555            let repr = self.repr_round_ref(base);
556            return Ok(repr.map(|v| FBig::new(v, *self)));
557        } else if base.significand.is_zero() {
558            // With a *float* exponent the result on a zero base is the positive one — this
559            // matches the common float-pow convention (e.g. CPython: `(-0.0) ** y == 0.0`),
560            // which doesn't track the parity of the exponent:
561            //   pow(±0, y > 0) = +0,    pow(±0, y < 0) = +inf.
562            // For the sign-correct result (e.g. `pow(-0, odd) = -0`), use the integer-exponent
563            // [`powi`](Context::powi). Short-circuiting here also avoids the negative-base path.
564            return Ok(Exact(if exp.sign() == Sign::Negative {
565                FBig::new(Repr::infinity(), *self)
566            } else {
567                FBig::ZERO
568            }));
569        }
570        if base.is_one() {
571            // pow(1, y) = 1 for any finite y (exp is finite here — infinities were rejected above).
572            return Ok(Exact(FBig::ONE));
573        }
574
575        // Integer-valued exponent: delegate to the integer-power kernel (binary exponentiation),
576        // itself correctly rounded via its own Ziv loop. This sidesteps the `exp(y·ln x)`
577        // amplification entirely, and lets a negative base through — `powi` fixes the sign from
578        // the exponent's parity. Gated on `is_int` (a cheap exponent check) so the non-integer
579        // common case skips `to_int`.
580        if exp.is_int() {
581            return self.powi(base, exp.to_int().value());
582        }
583
584        if base.sign() == Sign::Negative {
585            // A non-integer exponent on a negative base has no real value.
586            return Err(FpError::OutOfDomain);
587        }
588
589        // `base` is positive here (negative non-integer base returned OutOfDomain above).
590        self.pow_exp_log(base, exp, cache)
591    }
592
593    /// `x^y = exp(y·ln x)` for `pos_base > 0`, correctly rounded via a Ziv loop — the shared core
594    /// of [`powf`](Self::powf) (non-integer exponents) and the [`powi`](Self::powi) fallback for
595    /// exponents past the squaring chain's feasible range. `ln` and `exp` are themselves Ziv-correct
596    /// at the working precision, so the radius comes only from the rounding of the `ln`/`mul`/`exp`
597    /// chain — but `exp` AMPLIFIES the absolute error of its argument `y·ln x` by the result
598    /// magnitude, i.e. by a relative factor of `|y·ln x|`. The radius is
599    /// `result.ulp() · (|y·ln x| + 1) · (B + 8)` where `result.ulp()` is taken at the *working*
600    /// precision, so it shrinks as `B^{-guard}` and the containment test converges. (A radius
601    /// computed at unlimited precision would be constant across retries and never converge for a
602    /// value near a rounding boundary.) The `B + 8` scale covers the `ulp`-vs-`value·B^{1-P}` gap
603    /// plus a safety margin for the chained roundings.
604    ///
605    /// Overflow/underflow of `exp(y·ln x)` is detected inside the Ziv closure by `exp` itself
606    /// (which returns `Err(Overflow)` / `Err(Underflow)`) and propagated — the result is positive
607    /// (argument to `exp`), so overflow is `+∞` and underflow carries `+` sign; callers that need a
608    /// negative result (the `powi` fallback for a negative base) flip the sign of both the value
609    /// and the error.
610    fn pow_exp_log<const B: Word>(
611        &self,
612        pos_base: &Repr<B>,
613        exp: &Repr<B>,
614        mut cache: Option<&mut ConstCache>,
615    ) -> FpResult<FBig<R, B>> {
616        let initial_guard = self.base_guard_digits::<B>() + 10;
617        self.ziv(initial_guard, |guard| {
618            let wp = self.precision + guard;
619            // exp(y·ln x) as a Ball chain: the ln and the exponent rounding compose mechanically,
620            // and the exp input error is folded in via `exp_ball`. The radius shrinks with guard
621            // now that `ln_compute`'s s<0 reduction no longer inflates its error count.
622            let ln_ball = self.ln_compute::<B>(pos_base, wp, false, reborrow_cache(&mut cache));
623            let work = Context::<mode::HalfEven>::new(wp);
624            let exp_input =
625                Ball::from_rounded(work.repr_round_ref(exp).map(|r| FBig::new(r, work)));
626            let arg_ball = ln_ball.mul(&exp_input);
627            let result_ball = self.exp_ball::<B>(&arg_ball, reborrow_cache(&mut cache))?;
628            Ok(result_ball.to_value_radius::<R>())
629        })
630    }
631
632    /// `powi` fallback for exponents past the squaring chain's feasible bit length: compute
633    /// `base^signed_exp = exp(signed_exp · ln|base|)`. The integer exponent is an exact float
634    /// (significand `|signed_exp|`, exponent `0`), so this delegates to [`pow_exp_log`](Self::pow_exp_log)
635    /// — reusing its Ziv rounding — and then fixes the result sign from the
636    /// exponent's parity for a negative base. The working precision never scales with
637    /// `signed_exp.bit_length()`, so (unlike the squaring chain) this cannot allocate unboundedly.
638    fn powi_via_exp_log<const B: Word>(
639        &self,
640        base: &Repr<B>,
641        signed_exp: &IBig,
642    ) -> FpResult<FBig<R, B>> {
643        let neg_base = base.sign() == Sign::Negative;
644        // `(-base)^n = (-1)^n · base^n`, so the magnitude is always `|base|^n` and only the sign
645        // depends on parity. `pow_exp_log` returns the positive magnitude; flip it (and the
646        // overflow/underflow sign) when the base is negative and the exponent is odd.
647        let odd = signed_exp.clone().into_parts().1.bit(0);
648        let result_sign = if neg_base && odd {
649            Sign::Negative
650        } else {
651            Sign::Positive
652        };
653        let base_mag = if neg_base {
654            let (_, mag) = base.significand().clone().into_parts();
655            Repr::<B>::new(IBig::from_parts(Sign::Positive, mag), base.exponent())
656        } else {
657            base.clone()
658        };
659        let exp_repr = Repr::new(signed_exp.clone(), 0);
660        match self.pow_exp_log(&base_mag, &exp_repr, None) {
661            Ok(rounded) => Ok(rounded.map(|v| if result_sign == Sign::Negative { -v } else { v })),
662            Err(FpError::Overflow(_)) => Err(FpError::Overflow(result_sign)),
663            Err(FpError::Underflow(_)) => Err(FpError::Underflow(result_sign)),
664            Err(e) => Err(e),
665        }
666    }
667
668    /// Calculate the exponential function (`eˣ`) on the floating point number under this context.
669    ///
670    /// # Examples
671    ///
672    /// ```
673    /// # use dashu_base::ParseError;
674    /// # use dashu_float::DBig;
675    /// # use core::str::FromStr;
676    /// use dashu_base::Approximation::*;
677    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
678    ///
679    /// let context = Context::<HalfAway>::new(2);
680    /// let a = DBig::from_str("-1.234")?;
681    /// assert_eq!(context.exp(&a.repr(), None), Ok(Inexact(DBig::from_str("0.29")?, NoOp)));
682    /// # Ok::<(), ParseError>(())
683    /// ```
684    #[inline]
685    pub fn exp<const B: Word>(
686        &self,
687        x: &Repr<B>,
688        cache: Option<&mut ConstCache>,
689    ) -> FpResult<FBig<R, B>> {
690        if x.is_infinite() {
691            return Ok(Exact(FBig::new(
692                match x.sign() {
693                    Sign::Positive => Repr::infinity(),
694                    Sign::Negative => Repr::zero(),
695                },
696                *self,
697            )));
698        }
699        self.exp_internal(x, false, cache)
700    }
701
702    /// Calculate the exponential minus one function (`eˣ-1`) on the floating point number under this context.
703    ///
704    /// # Examples
705    ///
706    /// ```
707    /// # use dashu_base::ParseError;
708    /// # use dashu_float::DBig;
709    /// # use core::str::FromStr;
710    /// use dashu_base::Approximation::*;
711    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
712    ///
713    /// let context = Context::<HalfAway>::new(2);
714    /// let a = DBig::from_str("-0.1234")?;
715    /// assert_eq!(context.exp_m1(&a.repr(), None), Ok(Inexact(DBig::from_str("-0.12")?, SubOne)));
716    /// # Ok::<(), ParseError>(())
717    /// ```
718    #[inline]
719    pub fn exp_m1<const B: Word>(
720        &self,
721        x: &Repr<B>,
722        cache: Option<&mut ConstCache>,
723    ) -> FpResult<FBig<R, B>> {
724        if x.is_infinite() {
725            return match x.sign() {
726                Sign::Positive => Ok(Exact(FBig::new(Repr::infinity(), *self))),
727                Sign::Negative => Ok(Exact(-FBig::ONE)), // exp_m1(−∞) = −1
728            };
729        }
730        self.exp_internal(x, true, cache)
731    }
732
733    // The reduction stays in the *base-`B`* logarithm: `exp(x) = B^s · exp(r/Bⁿ)^(Bⁿ)` with
734    // `r = x − s·ln B`. A base-2 form (`r = x − s·ln 2`, powering `2ⁿ`) is deliberately **not**
735    // used: for a non-power-of-two base the `2^s` scaling is a multi-digit value (≈ s·log₂B⁻¹·log₁₀2
736    // digits) that would have to be materialized to multiply in, where the base-`B` form is an
737    // exact O(1) exponent shift; and for a power-of-two base the two forms coincide (`B = 2` ⇒
738    // `ln B = ln 2`, `B^s = 2^s`, `Bⁿ = 2ⁿ`), so this formulation is already the optimal one.
739    // `pow_exact(Bⁿ)` is binary exponentiation (~n·log₂B squarings), tracked exactly by the Ball.
740
741    fn exp_internal<const B: Word>(
742        &self,
743        x: &Repr<B>,
744        minus_one: bool,
745        mut cache: Option<&mut ConstCache>,
746    ) -> FpResult<FBig<R, B>> {
747        assert_finite(x);
748        let input_sign = x.sign();
749
750        if x.significand.is_zero() {
751            // exp(±0) = 1; exp_m1(±0) = ±0 (IEEE 754 §9.2.1 preserves the sign of zero).
752            // These exact results need no rounding, so handle them before the
753            // limited-precision assertion: a precision-0 (unlimited) FBig such as the
754            // one produced by `try_from(0.0)` must still compute exp/exp_m1 exactly.
755            return match minus_one {
756                false => Ok(Exact(FBig::ONE)),
757                true => {
758                    let zero = if input_sign == Sign::Negative {
759                        FBig::new(Repr::neg_zero(), Context::new(0))
760                    } else {
761                        FBig::ZERO
762                    };
763                    Ok(Exact(zero))
764                }
765            };
766        }
767
768        assert_limited_precision(self.precision);
769
770        // For sufficiently negative x, exp(x) is below half an ulp of -1, so exp_m1(x) is -1 plus a
771        // sub-ulp residual and its rounding is fully determined (Up/Zero -> the next representable
772        // above -1; the other modes -> -1). The Ziv loop cannot certify that result: the
773        // working-precision value collapses to exactly -1, and a directed rounding preimage is
774        // one-sided, so the containment test never resolves and the loop runs to its retry cap.
775        // Short-circuit to the same mode-aware endpoint used for the underflowed case. The cutoff is
776        // exp(x) < half-ulp(-1): -1 sits on a power-of-B boundary, so the spacing just below it is
777        // B^-p and the cutoff is |x| > p·ln(B) + ln 2. Compare the lower bound of log2|x| against an
778        // upper bound of log2(threshold) so a borderline input still falls through to Ziv (which
779        // converges there) rather than being mis-rounded.
780        if minus_one && input_sign == Sign::Negative {
781            let thresh = self.precision as f32 * B.log2_est() * core::f32::consts::LN_2
782                + core::f32::consts::LN_2;
783            if x.log2_bounds().0 > thresh.log2_bounds().1 {
784                return Ok(self.exp_extreme_negative::<B>());
785            }
786        }
787
788        // No-OOM magnitude gate: for an `x` whose exponent is near `isize::MAX`, the reduction
789        // quotient `s = floor(x/ln B)` in `exp_compute` would allocate a GB-scale `IBig`, so reject
790        // astronomical |x| here (via the cheap `log2_est` fast-skip) before the Ziv loop runs the
791        // division. The probe inflates `ln B` with the same `⌈log_B|x|⌉ + 2` extra digits
792        // `exp_compute` uses, so its `s` verdict matches the computation's. (`exp_compute` also
793        // re-checks `s.try_into()` as a gray-zone backstop, propagating an error if the gate and
794        // computation ever disagree — so a miss degrades to the directed endpoint, not a panic.)
795        if x.log2_est().abs() > EXP_OVERFLOW_PROBE_LOG2 {
796            let x_log2_ub = x.log2_bounds().1;
797            let extra = if x_log2_ub > 0.0 {
798                (x_log2_ub / B.log2_est()) as usize + 2
799            } else {
800                2
801            };
802            let probe = Context::<R>::new(self.precision + 64 + extra);
803            let logb = probe.ln_base::<B>(reborrow_cache(&mut cache));
804            let x_probe = FBig::new(probe.repr_round_ref(x).value(), probe);
805            let s_probe = x_probe.div_rem_euclid(logb).0;
806            if <isize as core::convert::TryFrom<IBig>>::try_from(s_probe).is_err() {
807                // exp(huge +) overflows to +∞ (the directed endpoint handles every mode); exp(huge −)
808                // is a positive value below the smallest representable, so it underflows (the directed
809                // endpoint gives +0 / smallest-positive); exp_m1(huge −) ≈ −1 stays a finite value
810                // just above −1 (the short-circuit above usually catches this first).
811                return if input_sign == Sign::Positive {
812                    Err(FpError::Overflow(Sign::Positive))
813                } else if minus_one {
814                    Ok(self.exp_extreme_negative::<B>())
815                } else {
816                    Err(FpError::Underflow(Sign::Positive))
817                };
818            }
819        }
820
821        // Correct rounding via the Ziv loop. Guards: log_B(p) for the series summation/squaring
822        // rounding, plus `n` for the Bⁿ powering amplification — halved from the pre-Ziv `2n`,
823        // since Ziv (not the guard count) now certifies correctness. `n ≈ √p` is derived from the
824        // target precision and is constant across retries.
825        let series_guard = self.base_guard_digits::<B>();
826        let n = 1usize << (self.precision.bit_len() / 2);
827        self.ziv(series_guard + n, |guard| {
828            Ok(self
829                .exp_compute::<B>(
830                    x,
831                    self.precision + guard,
832                    minus_one,
833                    n,
834                    reborrow_cache(&mut cache),
835                )?
836                .to_value_radius::<R>())
837        })
838    }
839
840    /// Directed-rounded `exp_m1(x)` when `x` is so large and negative that `exp(x)` has underflowed
841    /// below the smallest representable FBig (the reduction quotient `s = floor(x/ln B)` overflows
842    /// `isize`). `exp_m1(x) = exp(x) − 1` then lies in `(−1, −1 + B^{isize::MIN})` — pinned only up
843    /// to a sub-representable residual, so the directed rounding mode picks the endpoint of the bin
844    /// it falls in: a value just above `−1` rounds to `−1` under `Down`/`Away`/nearest, and to the
845    /// next representable above `−1` under `Up`/`Zero` (both round the magnitude down toward 0).
846    ///
847    /// (`exp` itself of such an `x` is handled earlier — it returns `Err(Underflow)`, whose directed
848    /// endpoint is the same `+0` / smallest-positive this used to produce inline.)
849    ///
850    /// `Round::round_low_part` decides the endpoint: fed `−1` with a positive sub-ulp residual, its
851    /// `AddOne`/`NoOp` verdict is exactly the "round up to the next representable / stay" decision.
852    /// (The literal significand arithmetic `round_low_part` would do is irrelevant here — only its
853    /// directional verdict is used.)
854    fn exp_extreme_negative<const B: Word>(&self) -> Rounded<FBig<R, B>> {
855        // exp_m1(huge −): −1 + (sub-representable positive) ⇒ just above −1.
856        match R::round_low_part(&IBig::NEG_ONE, Sign::Positive, || Ordering::Less) {
857            AddOne => {
858                // Next representable above −1 at this precision: −(B^p − 1) × B^(−p)
859                // (the largest p-digit significand at exponent −p, e.g. p=1,B=2 → −0.5).
860                let p = self.precision;
861                let next_mag = Repr::<B>::BASE.pow(p) - UBig::ONE;
862                let next = Repr::new(IBig::from_parts(Sign::Negative, next_mag), -(p as isize));
863                Inexact(FBig::new(next, *self), AddOne)
864            }
865            // Carry the input context: `−FBig::ONE` is precision 0, which would make a downstream op
866            // on the result panic via `assert_limited_precision(0)`.
867            _ => Inexact(FBig::new(Repr::<B>::neg_one(), *self), NoOp),
868        }
869    }
870}
871
872#[cfg(test)]
873mod tests {
874    use super::*;
875    use crate::round::mode;
876
877    #[test]
878    fn test_exp_overflow_is_infinity() {
879        let ctx = Context::<mode::HalfEven>::new(53);
880        // exp(huge) overflows the isize exponent range -> Overflow at Context level.
881        // Need x large enough that floor(x/ln2) > isize::MAX, i.e. x > ~2^62.5.
882        let huge = Repr::new(IBig::from(1) << 63, 0);
883        assert_eq!(ctx.exp::<2>(&huge, None), Err(FpError::Overflow(Sign::Positive)));
884
885        // exp(huge −) is a positive value below the smallest representable -> Underflow at the
886        // Context layer; the directed endpoint is +0 under HalfEven (nearest), smallest-positive
887        // under Up.
888        let neg = Repr::new(-(IBig::from(1) << 63), 0);
889        assert_eq!(ctx.exp::<2>(&neg, None), Err(FpError::Underflow(Sign::Positive)));
890        assert!(ctx.unwrap_fp(ctx.exp::<2>(&neg, None)).repr().is_pos_zero(), "HalfEven -> +0");
891        let up = Context::<mode::Up>::new(53);
892        let up_val = up.unwrap_fp(up.exp::<2>(&neg, None));
893        assert_eq!(up_val.repr().significand(), &IBig::from(1), "Up -> smallest positive");
894        assert_eq!(up_val.repr().exponent(), isize::MIN);
895
896        // exp_m1(huge negative) -> -1 (a finite value, not an error)
897        let m1 = ctx.exp_m1::<2>(&neg, None).unwrap().value();
898        assert_eq!(m1, -FBig::<mode::HalfEven>::ONE);
899    }
900
901    // Directed rounding at the extreme-negative underflow: exp(x) for huge negative x is
902    // a positive value below the smallest representable FBig; exp_m1(x) = exp(x) − 1 is just above
903    // −1. The blanket +0 / Exact(−1) saturation was mode-blind — it returned +0 under Up and
904    // Exact(−1) under Up for exp_m1, violating Up ≥ Down.
905    #[test]
906    fn test_exp_extreme_negative_directed() {
907        // x = -2^63, precision 1 (exactly representable).
908        let up = FBig::<mode::Up>::from_parts(-IBig::ONE, 63);
909        let down = FBig::<mode::Down>::from_parts(-IBig::ONE, 63);
910        assert_eq!(up.precision(), 1);
911
912        // exp(-2^63): Up → smallest positive (1 × 2^{isize::MIN}); Down → +0.
913        let up_exp = up.exp();
914        let down_exp = down.exp();
915        assert_eq!(up_exp.repr().significand(), &IBig::from(1));
916        assert_eq!(up_exp.repr().exponent(), isize::MIN);
917        assert!(down_exp.repr().is_pos_zero(), "Down exp(huge −) is +0");
918        assert!(up_exp > down_exp, "Up(exp) > Down(exp)");
919
920        // exp_m1(-2^63) ∈ (-1, -1/2): at precision 1, Up → -1/2 (next above -1); Down → -1.
921        let up_m1 = up
922            .context()
923            .exp_m1(up.repr(), None)
924            .expect("finite exp_m1 input");
925        let down_m1 = down
926            .context()
927            .exp_m1(down.repr(), None)
928            .expect("finite exp_m1 input");
929        let expected_up = FBig::<mode::Up>::from_parts(-IBig::ONE, -1); // -1/2
930        assert!(!matches!(up_m1, Exact(_)), "exp_m1(huge −) is inexact under Up");
931        assert!(!matches!(down_m1, Exact(_)), "exp_m1(huge −) is inexact under Down too");
932        assert_eq!(up_m1.value().repr(), expected_up.repr());
933        assert_eq!(down_m1.value(), -FBig::<mode::Down>::ONE);
934    }
935
936    // exp_m1 of a large negative x where the reduction quotient still fits isize (so the overflow
937    // short-circuit doesn't fire) but exp(x) is below the result precision. The true value is
938    // -1 + (sub-ulp residual), so directed/nearest rounding is fully determined; the directed
939    // preimage being one-sided means Ziv cannot certify it, so it is short-circuited to the
940    // mode-aware endpoint. Verifies the result matches the directed saturation at several
941    // magnitudes/precisions (and that it does not regress to a many-retry Ziv loop).
942    #[test]
943    fn test_exp_m1_large_negative_directed_saturation() {
944        for &(e, p) in &[(50i32, 2usize), (50, 53), (100, 53), (1000, 53), (100, 128)] {
945            let up = FBig::<mode::Up, 2>::from_parts(IBig::from(-1), e as isize)
946                .with_precision(p)
947                .value()
948                .exp_m1();
949            let down = FBig::<mode::Down, 2>::from_parts(IBig::from(-1), e as isize)
950                .with_precision(p)
951                .value()
952                .exp_m1();
953            // Up -> next representable above -1 = -(2^p - 1) * 2^-p; Down -> -1.
954            let next_up_mag = (IBig::from(1) << p) - IBig::from(1);
955            let expected_up = FBig::<mode::Up, 2>::from_parts(-next_up_mag, -(p as isize));
956            assert_eq!(up.repr(), expected_up.repr(), "Up exp_m1(-2^{e}) p={p}");
957            assert_eq!(
958                down.repr(),
959                FBig::<mode::Down, 2>::NEG_ONE.repr(),
960                "Down exp_m1(-2^{e}) p={p}"
961            );
962            // The endpoint must carry the input precision: a precision-0 result would make a
963            // downstream op panic via `assert_limited_precision(0)`.
964            assert_eq!(up.precision(), p, "Up exp_m1(-2^{e}) p={p} lost precision");
965            assert_eq!(down.precision(), p, "Down exp_m1(-2^{e}) p={p} lost precision");
966            assert!(up > down, "Up > Down for exp_m1(-2^{e}) p={p}");
967        }
968    }
969
970    // exp(huge −) saturates to +0 (or the smallest positive under Up/Away). The endpoint must
971    // carry the input precision too — the precision-0 `FBig::ZERO` previously returned here
972    // tripped `assert_limited_precision(0)` on a downstream op.
973    #[test]
974    fn test_exp_extreme_negative_endpoint_precision() {
975        for &(e, p) in &[(50i32, 53usize), (100, 53), (1000, 53), (100, 128)] {
976            let up = FBig::<mode::Up, 2>::from_parts(-IBig::ONE, e as isize)
977                .with_precision(p)
978                .value()
979                .exp();
980            let down = FBig::<mode::Down, 2>::from_parts(-IBig::ONE, e as isize)
981                .with_precision(p)
982                .value()
983                .exp();
984            assert_eq!(up.precision(), p, "Up exp(-2^{e}) p={p} lost precision");
985            assert_eq!(down.precision(), p, "Down exp(-2^{e}) p={p} lost precision");
986            // A downstream op must not panic on the saturated endpoint.
987            let _ = down.sqrt();
988            assert!(up > down, "Up > Down for exp(-2^{e}) p={p}");
989        }
990    }
991
992    // Directed underflow through `powi`/`powf` must match `exp` (pow(x,y) = exp(y·ln x)) so the
993    // `Up ≥ Down` invariant holds across them. Previously `pow` saturated to signed zero under
994    // every mode, so `Up(pow(10, huge−))` was `+0` while `Up(exp(huge−·ln 10))` was the smallest
995    // positive — the two disagreed on the same mathematical value.
996    #[test]
997    fn test_pow_directed_underflow() {
998        let p = 53;
999        // |exp| · log2(10) > isize::MAX ⇒ the result exponent falls below isize::MIN (underflow).
1000        let huge_neg = IBig::from(-9_000_000_000_000_000_000_i64);
1001
1002        // powi(10, huge−): positive tiny result. Up → smallest positive, Down/Zero → +0.
1003        let up = FBig::<mode::Up, 2>::from_parts(IBig::from(10), 0)
1004            .with_precision(p)
1005            .value()
1006            .powi(huge_neg.clone());
1007        let down = FBig::<mode::Down, 2>::from_parts(IBig::from(10), 0)
1008            .with_precision(p)
1009            .value()
1010            .powi(huge_neg.clone());
1011        let zero = FBig::<mode::Zero, 2>::from_parts(IBig::from(10), 0)
1012            .with_precision(p)
1013            .value()
1014            .powi(huge_neg.clone());
1015        assert_eq!(up.repr().significand(), &IBig::from(1));
1016        assert_eq!(up.repr().exponent(), isize::MIN);
1017        assert!(down.repr().is_pos_zero());
1018        assert!(zero.repr().is_pos_zero());
1019        assert!(up > down);
1020
1021        // powi(-10, huge odd −): negative tiny result. Up → -0, Down → smallest negative.
1022        let odd = IBig::from(-9_000_000_000_000_000_001_i64);
1023        let nup = FBig::<mode::Up, 2>::from_parts(IBig::from(-10), 0)
1024            .with_precision(p)
1025            .value()
1026            .powi(odd.clone());
1027        let ndown = FBig::<mode::Down, 2>::from_parts(IBig::from(-10), 0)
1028            .with_precision(p)
1029            .value()
1030            .powi(odd.clone());
1031        assert!(nup.repr().is_neg_zero());
1032        assert!(ndown.repr().sign() == Sign::Negative && ndown.repr().exponent() == isize::MIN);
1033        assert!(nup > ndown);
1034
1035        // powf(2, y) with |y| > isize::MAX underflows and agrees with exp(y·ln 2).
1036        let ymag = IBig::from(-10_000_000_000_000_000_000_i128);
1037        let y_up = FBig::<mode::Up, 2>::from_parts(ymag.clone(), 0)
1038            .with_precision(p)
1039            .value();
1040        let y_down = FBig::<mode::Down, 2>::from_parts(ymag.clone(), 0)
1041            .with_precision(p)
1042            .value();
1043        let pf_up = FBig::<mode::Up, 2>::from_parts(IBig::from(2), 0)
1044            .with_precision(p)
1045            .value()
1046            .powf(&y_up);
1047        let pf_down = FBig::<mode::Down, 2>::from_parts(IBig::from(2), 0)
1048            .with_precision(p)
1049            .value()
1050            .powf(&y_down);
1051        assert_eq!(pf_up.repr().significand(), &IBig::from(1));
1052        assert_eq!(pf_up.repr().exponent(), isize::MIN);
1053        assert!(pf_down.repr().is_pos_zero());
1054        // Same value as exp(y·ln 2) under the same mode.
1055        let ln2 = FBig::<mode::Up, 2>::from_parts(IBig::from(2), 0)
1056            .with_precision(p)
1057            .value()
1058            .ln();
1059        let exp_up = (&y_up * &ln2)
1060            .with_precision(p)
1061            .value()
1062            .with_rounding::<mode::Up>()
1063            .exp();
1064        assert_eq!(exp_up.repr(), pf_up.repr(), "powf and exp disagree on directed underflow");
1065    }
1066
1067    // Directed overflow through `exp`/`powi`: outward modes reach ±∞, inward modes (toward-zero,
1068    // opposite-infinity) saturate to the largest finite `(Bᵖ−1) × B^{isize::MAX}` — the all-ones
1069    // significand at the output precision, mirroring MPFR's `mpfr_setmax`. (Hyperbolic `sinh`/`cosh`
1070    // overflow under nearest is unchanged — still ±∞.)
1071    #[test]
1072    fn test_directed_overflow() {
1073        let p = 53usize;
1074        let max_sig = (IBig::ONE << p) - IBig::ONE;
1075
1076        // exp(2^63): overflows. Up -> +∞, Zero/Down -> largest finite.
1077        let huge = FBig::<mode::HalfEven, 2>::from_parts(IBig::ONE << 63, 0)
1078            .with_precision(p)
1079            .value();
1080        let up = huge.clone().with_rounding::<mode::Up>().exp();
1081        let zero = huge.clone().with_rounding::<mode::Zero>().exp();
1082        let down = huge.clone().with_rounding::<mode::Down>().exp();
1083        assert!(up.repr().is_infinite() && up.repr().sign() == Sign::Positive, "Up -> +∞");
1084        assert_eq!(zero.repr().significand(), &max_sig, "Zero -> largest finite significand");
1085        assert_eq!(zero.repr().exponent(), isize::MAX, "Zero -> largest finite exponent");
1086        assert_eq!(down.repr().significand(), &max_sig, "Down -> largest finite");
1087        assert_eq!(down.repr().exponent(), isize::MAX);
1088        assert!(up > zero, "Up(+∞) > largest finite");
1089
1090        // powi(-2, huge odd): negative overflow. Down -> -∞, Up -> largest finite negative.
1091        let odd = IBig::from(10_000_000_000_000_000_001_i128);
1092        let n_up = FBig::<mode::Up, 2>::from_parts(IBig::from(-2), 0)
1093            .with_precision(p)
1094            .value()
1095            .powi(odd.clone());
1096        let n_down = FBig::<mode::Down, 2>::from_parts(IBig::from(-2), 0)
1097            .with_precision(p)
1098            .value()
1099            .powi(odd.clone());
1100        assert!(
1101            n_down.repr().is_infinite() && n_down.repr().sign() == Sign::Negative,
1102            "Down -> -∞"
1103        );
1104        assert_eq!(
1105            n_up.repr().significand(),
1106            &(-max_sig.clone()),
1107            "Up -> largest finite negative significand"
1108        );
1109        assert_eq!(n_up.repr().exponent(), isize::MAX);
1110        assert_eq!(n_up.repr().sign(), Sign::Negative);
1111    }
1112
1113    // Overflow at unlimited precision panics: the largest finite is undefined (no precision cap),
1114    // so the directed endpoint can't be formed. Reached via `powi` with a positive exponent, which
1115    // skips the limited-precision assertion and lets the overflow reach `unwrap_fp`.
1116    #[test]
1117    #[should_panic(expected = "precision cannot be 0")]
1118    fn test_overflow_at_unlimited_precision_panics() {
1119        let base =
1120            FBig::<mode::Zero, 2>::from_repr(Repr::<2>::new(IBig::from(2), 0), Context::new(0));
1121        let _ = base.powi(IBig::from(10_000_000_000_000_000_000_i128));
1122    }
1123
1124    // Exponents past `i64` (bit length > `MAX_POWI_CHAIN_BITS`) can't use the squaring chain: for a
1125    // base near 1 the magnitude overflows the finite range mid-computation, and the chain's growing
1126    // working precision exhausts memory. The `exp(y·ln x)` fallback computes these without scaling
1127    // the working precision with the exponent's bit length, returning the correct directed endpoint.
1128    #[test]
1129    fn test_powi_huge_exponent_fallback() {
1130        let p = 53usize;
1131        let max_sig = (IBig::ONE << p) - IBig::ONE;
1132        // base = 1 + 2^-52 (just above 1); exponent 2^200 has bit length 201.
1133        let near1 = 0x3ff0_0000_0000_0001u64;
1134        let huge_pos = IBig::ONE << 200usize;
1135        let huge_neg = -(IBig::ONE << 200usize);
1136
1137        // positive base, positive huge exp -> positive overflow. Up -> +∞, Down -> largest finite.
1138        let up = FBig::<mode::Up, 2>::try_from(f64::from_bits(near1))
1139            .unwrap()
1140            .with_precision(p)
1141            .value()
1142            .powi(huge_pos.clone());
1143        let down = FBig::<mode::Down, 2>::try_from(f64::from_bits(near1))
1144            .unwrap()
1145            .with_precision(p)
1146            .value()
1147            .powi(huge_pos.clone());
1148        let he = FBig::<mode::HalfEven, 2>::try_from(f64::from_bits(near1))
1149            .unwrap()
1150            .with_precision(p)
1151            .value()
1152            .powi(huge_pos.clone());
1153        assert!(up.repr().is_infinite() && up.repr().sign() == Sign::Positive, "Up -> +∞");
1154        assert_eq!(down.repr().significand(), &max_sig, "Down -> largest finite");
1155        assert_eq!(down.repr().exponent(), isize::MAX);
1156        assert!(he.repr().is_infinite() && he.repr().sign() == Sign::Positive, "nearest -> +∞");
1157        assert!(up > down);
1158
1159        // positive base, negative huge exp -> underflow. Up -> smallest positive, Down -> +0.
1160        let u_up = FBig::<mode::Up, 2>::try_from(f64::from_bits(near1))
1161            .unwrap()
1162            .with_precision(p)
1163            .value()
1164            .powi(huge_neg.clone());
1165        let u_down = FBig::<mode::Down, 2>::try_from(f64::from_bits(near1))
1166            .unwrap()
1167            .with_precision(p)
1168            .value()
1169            .powi(huge_neg.clone());
1170        assert_eq!(u_up.repr().significand(), &IBig::from(1));
1171        assert_eq!(u_up.repr().exponent(), isize::MIN);
1172        assert!(u_down.repr().is_pos_zero());
1173        assert!(u_up > u_down);
1174
1175        // negative base near -1, huge exponent: sign follows the exponent's parity. The magnitude
1176        // overflows, so nearest reaches ±∞ with the parity sign.
1177        let neg_near1 = 0xbff0_0000_0000_0001u64;
1178        let even = huge_pos.clone();
1179        let odd = (IBig::ONE << 200usize) + IBig::ONE;
1180        let he_even = FBig::<mode::HalfEven, 2>::try_from(f64::from_bits(neg_near1))
1181            .unwrap()
1182            .with_precision(p)
1183            .value()
1184            .powi(even);
1185        let he_odd = FBig::<mode::HalfEven, 2>::try_from(f64::from_bits(neg_near1))
1186            .unwrap()
1187            .with_precision(p)
1188            .value()
1189            .powi(odd.clone());
1190        assert!(
1191            he_even.repr().is_infinite() && he_even.repr().sign() == Sign::Positive,
1192            "even exponent -> +∞"
1193        );
1194        assert!(
1195            he_odd.repr().is_infinite() && he_odd.repr().sign() == Sign::Negative,
1196            "odd exponent -> -∞"
1197        );
1198        // Down of a negative overflow rounds toward -∞.
1199        let down_odd = FBig::<mode::Down, 2>::try_from(f64::from_bits(neg_near1))
1200            .unwrap()
1201            .with_precision(p)
1202            .value()
1203            .powi(odd);
1204        assert!(
1205            down_odd.repr().is_infinite() && down_odd.repr().sign() == Sign::Negative,
1206            "Down(negative overflow) -> -∞"
1207        );
1208    }
1209
1210    // Directed `powi` on a tiny base with a large negative exponent: base ≈ -3.6e-5, exponent
1211    // -2^31. The result (≈2^3.17e10) is representable on 64-bit `isize` (MAX ≈ 9.2e18), so this
1212    // exercises the squaring chain (bit length 32 ≤ MAX_POWI_CHAIN_BITS) and must return a finite
1213    // value promptly — a regression guard for the chain path. On 32-bit `isize` (MAX ≈ 2.1e9) the
1214    // same result overflows and is short-circuited by the range guard, so the guard is 64-bit-only.
1215    #[cfg(target_pointer_width = "64")]
1216    #[test]
1217    fn test_powi_reproducer_small_base_representable() {
1218        let base_bits = 0xbf02e3ff24ffff1fu64;
1219        let exp = IBig::from(-(1i64 << 31));
1220        for p in [20usize, 50, 100, 500] {
1221            let v = FBig::<mode::HalfEven, 2>::try_from(f64::from_bits(base_bits))
1222                .unwrap()
1223                .with_precision(p)
1224                .value()
1225                .powi(exp.clone());
1226            assert!(!v.repr().is_infinite(), "finite at p={p}");
1227            assert_eq!(v.repr().sign(), Sign::Positive, "even exponent -> positive at p={p}");
1228            // magnitude ≈ 2^3.17e10, far from 1 — the chain computed the huge representable value.
1229            assert!(v.repr().exponent() > 1_000_000_000, "huge magnitude at p={p}");
1230        }
1231    }
1232
1233    // `powi(2, exp)` for `exp` just below `isize::MAX`: the result `2^exp` is representable (its
1234    // exponent is `exp ≤ isize::MAX`), so the range guard correctly does not short-circuit it and
1235    // the squaring chain computes it. The Ziv containment test then compares Reprs at that extreme
1236    // magnitude — `repr_cmp_same_base` used to do `exponent + digits` in plain `isize`, which
1237    // overflows near the ceiling and aborted (the raw-backend `backend_float_extremes` crash).
1238    #[test]
1239    fn test_powi_power_of_two_near_ceiling() {
1240        for offset in [1isize, 2, 60, 100, 1000] {
1241            let exp = IBig::from(isize::MAX - offset);
1242            let up = FBig::<mode::Up, 2>::from_parts(IBig::ONE, 1)
1243                .with_precision(53)
1244                .value()
1245                .powi(exp.clone());
1246            let down = FBig::<mode::Down, 2>::from_parts(IBig::ONE, 1)
1247                .with_precision(53)
1248                .value()
1249                .powi(exp.clone());
1250            // 2^exp is an exact power of two: significand 1 at exponent exp, identical under Up/Down.
1251            assert!(!up.repr().is_infinite(), "finite for offset {offset}");
1252            assert_eq!(up.repr().significand(), &IBig::from(1), "sig 1 for offset {offset}");
1253            assert_eq!(up.repr().exponent(), isize::MAX - offset, "exponent for offset {offset}");
1254            assert_eq!(down.repr().exponent(), isize::MAX - offset);
1255            assert!(!up.repr().is_infinite() && !down.repr().is_infinite());
1256        }
1257
1258        // Symmetric floor: `powi(2, -exp)` for `exp` just below `isize::MAX` gives `2^-exp`, whose
1259        // exponent sits just above `isize::MIN`. The containment test compares Reprs there too — the
1260        // saturating `cmp` shortcuts cover the floor as well as the ceiling (this is the negative-
1261        // exponent half of the range-handling TODO).
1262        for offset in [1isize, 2, 60, 100, 1000] {
1263            let exp = IBig::from(-(isize::MAX - offset));
1264            let up = FBig::<mode::Up, 2>::from_parts(IBig::ONE, 1)
1265                .with_precision(53)
1266                .value()
1267                .powi(exp.clone());
1268            let down = FBig::<mode::Down, 2>::from_parts(IBig::ONE, 1)
1269                .with_precision(53)
1270                .value()
1271                .powi(exp);
1272            assert!(!up.repr().is_infinite(), "finite for floor offset {offset}");
1273            assert_eq!(up.repr().significand(), &IBig::from(1), "sig 1 for floor offset {offset}");
1274            assert_eq!(up.repr().exponent(), -(isize::MAX - offset));
1275            assert!(!down.repr().is_infinite());
1276        }
1277    }
1278
1279    // `powi(2, isize::MAX)`: the result `2^isize::MAX` normalizes to significand 1 at the `+inf`
1280    // sentinel exponent, so it is genuine overflow. This used to panic — the squaring chain absorbed
1281    // the overflow into an infinity and the Ziv closure then choked on `res.ulp()` of an infinity.
1282    // Now the chain propagates the range error and `powi` returns the directed endpoint for every
1283    // mode. `isize::MAX` is the sentinel on both pointer widths, so this test is arch-independent.
1284    #[test]
1285    fn test_powi_exact_ceiling_overflow_directed() {
1286        let max_sig = |p: usize| (IBig::ONE << p) - IBig::ONE;
1287        let exp = IBig::from(isize::MAX);
1288        for p in [20usize, 50, 100, 500] {
1289            // Context layer: genuine overflow, positive sign.
1290            let ctx = Context::<mode::HalfEven>::new(p);
1291            let base = Repr::<2>::new(IBig::from(2), 0);
1292            assert_eq!(
1293                ctx.powi::<2>(&base, exp.clone()),
1294                Err(FpError::Overflow(Sign::Positive)),
1295                "Overflow at p={p}"
1296            );
1297
1298            // Convenience layer: directed endpoints (outward → +∞, inward → largest finite).
1299            let he = FBig::<mode::HalfEven, 2>::from_parts(IBig::ONE, 1)
1300                .with_precision(p)
1301                .value()
1302                .powi(exp.clone());
1303            let up = FBig::<mode::Up, 2>::from_parts(IBig::ONE, 1)
1304                .with_precision(p)
1305                .value()
1306                .powi(exp.clone());
1307            let down = FBig::<mode::Down, 2>::from_parts(IBig::ONE, 1)
1308                .with_precision(p)
1309                .value()
1310                .powi(exp.clone());
1311            let zero = FBig::<mode::Zero, 2>::from_parts(IBig::ONE, 1)
1312                .with_precision(p)
1313                .value()
1314                .powi(exp.clone());
1315            assert!(
1316                he.repr().is_infinite() && he.repr().sign() == Sign::Positive,
1317                "HalfEven -> +∞ at p={p}"
1318            );
1319            assert!(
1320                up.repr().is_infinite() && up.repr().sign() == Sign::Positive,
1321                "Up -> +∞ at p={p}"
1322            );
1323            assert_eq!(down.repr().significand(), &max_sig(p), "Down -> largest finite at p={p}");
1324            assert_eq!(down.repr().exponent(), isize::MAX, "Down exponent at p={p}");
1325            assert_eq!(zero.repr().significand(), &max_sig(p), "Zero -> largest finite at p={p}");
1326            assert!(up > down, "Up >= Down at p={p}");
1327        }
1328    }
1329
1330    // Underflow propagation through the chain: a tiny base `2^(isize::MIN/2)` squared reaches the
1331    // `-inf` sentinel exponent (`isize::MIN`) on the first squaring, so the chain underflows and
1332    // `powi` routes it to the directed endpoint instead of panicking. (Note `powi(2, -isize::MAX)` is
1333    // *not* underflow — `2^-isize::MAX` sits at exponent `isize::MIN+1`, still representable — so this
1334    // case uses a base whose square genuinely crosses the floor.) `isize::MIN/2` scales with the
1335    // pointer width, keeping the test arch-independent.
1336    #[test]
1337    fn test_powi_chain_underflow_propagates() {
1338        let half_floor = isize::MIN / 2;
1339        let ctx = Context::<mode::HalfEven>::new(53);
1340        let tiny = Repr::<2>::new(IBig::ONE, half_floor);
1341        assert_eq!(
1342            ctx.powi::<2>(&tiny, IBig::from(2)),
1343            Err(FpError::Underflow(Sign::Positive)),
1344            "base^2 underflows to the floor sentinel"
1345        );
1346
1347        // Convenience layer: directed endpoints (outward → smallest positive, inward/nearest → +0).
1348        let he = FBig::<mode::HalfEven, 2>::from_parts(IBig::ONE, half_floor)
1349            .with_precision(53)
1350            .value()
1351            .powi(IBig::from(2));
1352        let up = FBig::<mode::Up, 2>::from_parts(IBig::ONE, half_floor)
1353            .with_precision(53)
1354            .value()
1355            .powi(IBig::from(2));
1356        let down = FBig::<mode::Down, 2>::from_parts(IBig::ONE, half_floor)
1357            .with_precision(53)
1358            .value()
1359            .powi(IBig::from(2));
1360        assert!(he.repr().is_pos_zero(), "HalfEven -> +0");
1361        assert_eq!(up.repr().significand(), &IBig::from(1), "Up -> smallest positive");
1362        assert_eq!(up.repr().exponent(), isize::MIN, "Up exponent");
1363        assert!(down.repr().is_pos_zero(), "Down -> +0");
1364        assert!(up > down, "Up >= Down");
1365    }
1366
1367    // Significand != 1 and negative-base sign handling at the ceiling: the magnitude overflows just
1368    // the same and must propagate (not panic), with the overflow sign following base sign × parity.
1369    #[test]
1370    fn test_powi_significand_nonunit_ceiling() {
1371        let ctx = Context::<mode::HalfEven>::new(53);
1372        // base 3: 3^isize::MAX overflows with a positive sign.
1373        let base3 = Repr::<2>::new(IBig::from(3), 0);
1374        assert_eq!(
1375            ctx.powi::<2>(&base3, IBig::from(isize::MAX)),
1376            Err(FpError::Overflow(Sign::Positive)),
1377            "3^MAX overflows positive"
1378        );
1379        // base -2, odd exponent isize::MAX: (-2)^odd is negative -> Overflow(Negative).
1380        let neg2 = Repr::<2>::new(IBig::from(-2), 0);
1381        assert_eq!(
1382            ctx.powi::<2>(&neg2, IBig::from(isize::MAX)),
1383            Err(FpError::Overflow(Sign::Negative)),
1384            "(-2)^MAX overflows negative"
1385        );
1386    }
1387
1388    // Regression guard: a negative base with an *even* exponent just below the ceiling is
1389    // representable (positive, magnitude 2^(isize::MAX-1)) and must still compute to a finite value
1390    // — the overflow propagation must not over-broaden and treat near-ceiling representable results
1391    // as overflow. `isize::MAX - 1` is even on both 32- and 64-bit.
1392    #[test]
1393    fn test_powi_negative_base_even_exp_near_ceiling() {
1394        let exp = IBig::from(isize::MAX - 1);
1395        for p in [20usize, 50, 100, 500] {
1396            let v = FBig::<mode::HalfEven, 2>::try_from(-2.0f64)
1397                .unwrap()
1398                .with_precision(p)
1399                .value()
1400                .powi(exp.clone());
1401            assert!(!v.repr().is_infinite(), "finite at p={p}");
1402            assert_eq!(v.repr().sign(), Sign::Positive, "even exponent -> positive at p={p}");
1403            assert_eq!(v.repr().significand(), &IBig::from(1), "sig 1 at p={p}");
1404            assert_eq!(v.repr().exponent(), isize::MAX - 1, "exponent at p={p}");
1405        }
1406    }
1407
1408    // A sharp OOM regression needs an exponent gap large enough that 2^gap exceeds any
1409    // memory (gap ≳ 1e11), yet with floor(x/ln2) still fitting isize so the overflow
1410    // branch is not taken. That window only exists where isize is 64-bit: on 32-bit,
1411    // isize tops out at ~2.1e9 — below any OOM-inducing gap — so the overflow branch
1412    // always intervenes first. The fix itself (log2_bounds in round_fract) is
1413    // arch-independent; only this dedicated sharp test is 64-bit-only.
1414    #[test]
1415    fn test_exact_results_on_unlimited_precision() {
1416        // Regression test: values carrying precision 0 (unlimited) — produced by
1417        // `try_from(0.0)` and the `FBig::ONE`/`ZERO` constants — must still compute
1418        // their exact-result special cases instead of panicking in
1419        // assert_limited_precision before reaching the shortcut.
1420        type F = FBig<mode::HalfEven, 2>;
1421
1422        let zero = F::try_from(0.0_f64).unwrap();
1423        assert_eq!(zero.exp(), F::ONE);
1424        assert_eq!(zero.exp_m1(), F::ZERO);
1425        assert_eq!(zero.sqrt(), F::ZERO);
1426        assert_eq!(zero.ln_1p(), F::ZERO);
1427
1428        // -0.0 preserves its sign through exp_m1 and sqrt.
1429        let neg_zero = F::try_from(-0.0_f64).unwrap();
1430        assert!(neg_zero.exp_m1().repr().is_neg_zero());
1431        assert!(neg_zero.sqrt().repr().is_neg_zero());
1432
1433        // FBig::ONE carries unlimited precision; ln(1) = 0 is exact.
1434        assert_eq!(F::ONE.ln(), F::ZERO);
1435    }
1436
1437    #[test]
1438    fn test_powf_zero_base() {
1439        use crate::DBig;
1440        // powf with a float exponent returns the *positive* result on a zero base
1441        // (matching the common float-pow convention); use powi for the signed result.
1442        let ctx = Context::<mode::HalfEven>::new(53);
1443        // powf(-0, 2.0) = +0 (NOT -0)
1444        let r = ctx
1445            .powf::<2>(&Repr::<2>::neg_zero(), &Repr::new(2.into(), 0), None)
1446            .unwrap()
1447            .value();
1448        assert!(r.repr().is_pos_zero(), "expected +0");
1449        assert!(!r.repr().is_neg_zero(), "powf(-0, x) should be +0, not -0");
1450        // powf(0, -1) = +inf
1451        let r = ctx
1452            .powf::<2>(&Repr::<2>::zero(), &Repr::new((-1i32).into(), 0), None)
1453            .unwrap()
1454            .value();
1455        assert!(r.repr().is_infinite());
1456        assert_eq!(r.repr().sign(), Sign::Positive);
1457        // powi(-0, 3) = -0 (the sign-correct, integer-exponent variant)
1458        let r = ctx
1459            .powi::<2>(&Repr::<2>::neg_zero(), 3.into())
1460            .unwrap()
1461            .value();
1462        assert!(r.repr().is_neg_zero());
1463        let _ = DBig::ZERO;
1464    }
1465
1466    #[test]
1467    fn test_powf_integer_exponent() {
1468        use crate::DBig;
1469        let ctx = Context::<mode::HalfEven>::new(53);
1470        // integer-valued float exponent delegates to powi and supports a negative base (its sign
1471        // is fixed by the exponent's parity): (-5)^3 = -125.
1472        let neg_base = &Repr::<2>::new((-5).into(), 0);
1473        let exp3 = &Repr::<2>::new(3.into(), 0);
1474        let via_powf = ctx.powf::<2>(neg_base, exp3, None).unwrap().value();
1475        let via_powi = ctx.powi::<2>(neg_base, 3.into()).unwrap().value();
1476        assert_eq!(via_powf.repr(), via_powi.repr());
1477        assert_eq!(via_powf.repr().sign(), Sign::Negative);
1478
1479        // a non-integer exponent on a negative base is out of domain (no real value)
1480        let exp_half = &Repr::<2>::new(5.into(), -1); // 2.5
1481        assert_eq!(ctx.powf::<2>(neg_base, exp_half, None), Err(FpError::OutOfDomain));
1482
1483        // positive base, integer exponent: also routes through powi
1484        let pos_base = &Repr::<2>::new(3.into(), 0);
1485        let exp4 = &Repr::<2>::new(4.into(), 0);
1486        let r = ctx.powf::<2>(pos_base, exp4, None).unwrap().value();
1487        assert_eq!(r.repr(), ctx.powi::<2>(pos_base, 4.into()).unwrap().value().repr());
1488        let _ = DBig::ZERO;
1489    }
1490
1491    #[test]
1492    fn exp_ball_bounds_propagated_input_error() {
1493        // The input ball's error must be amplified by |exp| in the result radius: n·ulp(exp)
1494        // has to cover n_x·ulp(x)·|exp(x)|. Regression for the missing `sig_r` factor in
1495        // `exp_ball`'s inflate term, which under-bound the radius by ~sig_r (≈ B^(p−1)) and let
1496        // Ziv certify an interval that did not contain the true value.
1497        type F = FBig<mode::HalfEven, 10>;
1498        let ctx = Context::<mode::HalfEven>::new(10);
1499        // mid = 0.5 at precision 10 (ulp = 1e-10), n = 10 ⇒ true arg = 0.5000000010.
1500        let mid = F::from_parts(IBig::from(5000000000i64), -10)
1501            .with_precision(10)
1502            .value();
1503        let x = Ball::<10>::with_error(mid, IBig::from(10));
1504        let r = ctx.exp_ball::<10>(&x, None).unwrap();
1505        let true_arg = F::from_parts(IBig::from(5000000010i64), -10)
1506            .with_precision(0)
1507            .value();
1508        let exp_true = true_arg
1509            .with_precision(60)
1510            .value()
1511            .exp()
1512            .with_precision(0)
1513            .value();
1514        let diff = (r.mid.clone().with_precision(0).value() - exp_true).abs();
1515        let bound = F::from(r.n.clone()) * r.mid.ulp().with_precision(0).value();
1516        assert!(
1517            diff <= bound,
1518            "exp_ball: |mid − true| = {diff} > n·ulp = {bound} (n = {}, missing sig_r?)",
1519            r.n
1520        );
1521    }
1522
1523    #[test]
1524    fn exp_generic_base_matches_oracle() {
1525        // exp at a generic (uncached) base exercises `ln_base_ball`'s mechanical-radius path for
1526        // ln(B) — the hard-coded `8`-ulp bound would under-bind a generic base's atanh-series
1527        // error (which is ~series-terms ulps), silently unsounding the reduction.
1528        type F3 = FBig<mode::HalfEven, 3>;
1529        for x in [1i64, 2, 3, 5] {
1530            let x = F3::from_parts(IBig::from(x), 0);
1531            let ctx = Context::<mode::HalfEven>::new(30);
1532            let got = ctx.exp::<3>(&x.repr, None).unwrap().value();
1533            let oracle = Context::<mode::HalfEven>::new(90)
1534                .exp::<3>(&x.repr, None)
1535                .unwrap()
1536                .value();
1537            let want = ctx.repr_round_ref(&oracle.repr).value();
1538            assert_eq!(got.repr, want, "exp({x:?}) at base 3 p=30: got {got:?}, want {want:?}");
1539        }
1540    }
1541}