Skip to main content

dashu_float/
exp.rs

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