Skip to main content

dashu_float/
exp.rs

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