Skip to main content

dashu_float/
exp.rs

1use core::convert::TryInto;
2
3use crate::{
4    error::{assert_finite, assert_limited_precision, FpError, FpResult},
5    fbig::FBig,
6    math::cache::{reborrow_cache, ConstCache},
7    repr::{Context, Repr, Word},
8    round::{ErrorBounds, Round},
9};
10use dashu_base::{Abs, AbsOrd, Approximation::*, BitTest, DivRemEuclid, EstimatedLog2, Sign};
11use dashu_int::{IBig, UBig};
12
13// `powi` (integer power), `powf`/`exp`/`exp_m1` route through Ziv-backed Context methods, which
14// require `R: ErrorBounds` for their correctness guarantee.
15impl<R: ErrorBounds, const B: Word> FBig<R, B> {
16    /// Raise the floating point number to an integer power.
17    ///
18    /// # Examples
19    ///
20    /// ```
21    /// # use dashu_base::ParseError;
22    /// # use dashu_float::DBig;
23    /// # use core::str::FromStr;
24    /// let a = DBig::from_str("-1.234")?;
25    /// assert_eq!(a.powi(10.into()), DBig::from_str("8.188")?);
26    /// # Ok::<(), ParseError>(())
27    /// ```
28    #[inline]
29    pub fn powi(&self, exp: IBig) -> FBig<R, B> {
30        self.context.unwrap_fp(self.context.powi(&self.repr, exp))
31    }
32
33    /// Raise the floating point number to an floating point power.
34    ///
35    /// # Examples
36    ///
37    /// ```
38    /// # use dashu_base::ParseError;
39    /// # use dashu_float::DBig;
40    /// # use core::str::FromStr;
41    /// let x = DBig::from_str("1.23")?;
42    /// let y = DBig::from_str("-4.56")?;
43    /// assert_eq!(x.powf(&y), DBig::from_str("0.389")?);
44    /// # Ok::<(), ParseError>(())
45    /// ```
46    #[inline]
47    pub fn powf(&self, exp: &Self) -> Self {
48        let context = Context::max(self.context, exp.context);
49        context.unwrap_fp(context.powf(&self.repr, &exp.repr, None))
50    }
51
52    /// Calculate the exponential function (`eˣ`) on the floating point number.
53    ///
54    /// # Examples
55    ///
56    /// ```
57    /// # use dashu_base::ParseError;
58    /// # use dashu_float::DBig;
59    /// # use core::str::FromStr;
60    /// let a = DBig::from_str("-1.234")?;
61    /// assert_eq!(a.exp(), DBig::from_str("0.2911")?);
62    /// # Ok::<(), ParseError>(())
63    /// ```
64    #[inline]
65    pub fn exp(&self) -> FBig<R, B> {
66        self.context.unwrap_fp(self.context.exp(&self.repr, None))
67    }
68
69    /// Calculate the exponential minus one function (`eˣ-1`) on the floating point number.
70    ///
71    /// # Examples
72    ///
73    /// ```
74    /// # use dashu_base::ParseError;
75    /// # use dashu_float::DBig;
76    /// # use core::str::FromStr;
77    /// let a = DBig::from_str("-0.1234")?;
78    /// assert_eq!(a.exp_m1(), DBig::from_str("-0.11609")?);
79    /// # Ok::<(), ParseError>(())
80    /// ```
81    #[inline]
82    pub fn exp_m1(&self) -> FBig<R, B> {
83        self.context
84            .unwrap_fp(self.context.exp_m1(&self.repr, None))
85    }
86}
87
88impl<R: Round> Context<R> {
89    /// Left-to-right binary exponentiation of `start` to the power `n` (`n ≥ 2`) at this context's
90    /// precision — the shared squaring kernel.
91    ///
92    /// Each `sqr`/`mul` is correctly rounded, but their rounding flags are folded away (`.value()`)
93    /// and no containment test is applied, so the result is only *near*-correct: repeated squaring
94    /// compounds the relative error (it roughly doubles per step), so after `n.bit_len()` squarings
95    /// the error is on the order of `2^nlen · ulp`. The public [`powi`](Context::powi) retries this
96    /// kernel inside a Ziv loop to certify the rounding; `exp_compute` also uses it for its internal
97    /// `Bⁿ` powering (where the outer `exp` Ziv loop absorbs the error).
98    ///
99    /// Returns the value together with an `exact` flag that is `true` only when **every** squaring
100    /// and multiplication rounded `Exact` (so the returned value is the mathematically exact
101    /// `startⁿ`). The Ziv caller uses this to report a zero radius for exact results — under
102    /// directed rounding modes an exactly-representable result sits on a one-sided rounding
103    /// boundary, which a nonzero radius can never certify.
104    pub(crate) fn powi_chain<const B: Word>(
105        &self,
106        start: &Repr<B>,
107        n: &UBig,
108    ) -> (FBig<R, B>, bool) {
109        let nlen = n.bit_len();
110        debug_assert!(nlen >= 2, "powi_chain requires n >= 2");
111        let mut p = nlen - 2;
112        let first = self.sqr(start);
113        let mut exact = matches!(first, Ok(Exact(_)));
114        let mut res = self.unwrap_fp(first);
115        loop {
116            if n.bit(p) {
117                let m = self.mul(res.repr(), start);
118                exact = exact && matches!(m, Ok(Exact(_)));
119                res = self.unwrap_fp(m);
120            }
121            if p == 0 {
122                break;
123            }
124            p -= 1;
125            let s = self.sqr(res.repr());
126            exact = exact && matches!(s, Ok(Exact(_)));
127            res = self.unwrap_fp(s);
128        }
129        (res, exact)
130    }
131
132    /// Near-correct exp core: evaluate `exp(x)` (or `exp_m1(x)` when `minus_one`) at
133    /// `work_precision`, returning `(value, error_radius)`.
134    ///
135    /// Shared by the Ziv-backed `exp`/`exp_m1` (which retry it) and usable directly where only a
136    /// near-correct value is needed. The caller must have pre-checked that the reduction quotient
137    /// `s = floor(x/ln B)` fits `isize` (astronomical `|x|` overflows and is handled before the
138    /// Ziv loop, since this closure can't return `Err`). `n` (the reduction power, `≈ √p`) is
139    /// derived from the *target* precision and is constant across retries.
140    pub(crate) fn exp_compute<const B: Word>(
141        &self,
142        x: &Repr<B>,
143        work_precision: usize,
144        minus_one: bool,
145        n: usize,
146        mut cache: Option<&mut ConstCache>,
147    ) -> (FBig<R, B>, FBig<R, B>) {
148        // exp(x) = B^s · exp(r)^(Bⁿ), with r = x − s·ln(B) reduced so |r| < B⁻ⁿ.
149        let context = Context::<R>::new(work_precision);
150        let x = FBig::new(context.repr_round_ref(x).value(), context);
151
152        // When minus_one is true and |x| < 1/B, evaluate the Maclaurin series without scaling
153        // (no Bⁿ reduction, no powering — n_eff = 0).
154        let no_scaling = minus_one && x.log2_est() < -B.log2_est();
155
156        let (s, r, n_eff) = if no_scaling {
157            (0isize, x, 0usize)
158        } else {
159            let logb = context.ln_base::<B>(reborrow_cache(&mut cache));
160            let (s_big, r) = x.div_rem_euclid(logb);
161            let s: isize = s_big
162                .try_into()
163                .expect("exp reduction quotient fits isize (overflow pre-checked)");
164            (s, r, n)
165        };
166        let r = r >> n_eff as isize;
167
168        // Maclaurin series: exp(r) = 1 + Σ rⁱ/i!
169        let mut factorial = IBig::ONE;
170        let mut pow = r.clone();
171        let mut sum = if no_scaling {
172            r.clone()
173        } else {
174            FBig::ONE + &r
175        };
176        let mut k = 2u32;
177        let mut terms: usize = 1;
178        loop {
179            factorial *= k;
180            pow *= &r;
181
182            let increase = &pow / &factorial;
183            if increase.abs_cmp(&sum.ulp_lb()).is_le() {
184                break;
185            }
186            sum += increase;
187            k += 1;
188            terms += 1;
189        }
190
191        // The radius is computed at *unlimited* precision so the bound arithmetic is exact — a
192        // work-precision product would drop digits and could under-estimate (a soundness hole).
193        let ulp_w = || sum.ulp().with_precision(0).value();
194
195        if no_scaling {
196            // exp_m1(x) = sum directly; error is the series truncation + rounding.
197            let radius = ulp_w() * (4 * terms + 8) + ulp_w();
198            (sum, radius)
199        } else {
200            // Powering amplifies the series' relative error by Bⁿ. With |v|/|sum| < e < 3 (both
201            // near 1, since |r| < B⁻ⁿ), |v − true| ≤ 3·Bⁿ·(4K+8)·ulp(sum) + ulp(v). The B^s shift
202            // is exact, so the bound shifts with the value.
203            //
204            // The squaring chain compounds the relative error (it doubles per step), so it is run
205            // at an inflated precision and rounded back to `work_precision` — near-correct, which
206            // is what the `+ ulp(v)` term above accounts for.
207            let bn: UBig = Repr::<B>::BASE.pow(n);
208            let chain_ctx =
209                Context::<R>::new(work_precision + bn.bit_len() + work_precision.bit_len());
210            let (v_pow, _) = chain_ctx.powi_chain(sum.repr(), &bn);
211            let v = v_pow.with_precision(work_precision).value();
212            let v_shifted = v.clone() << s;
213            let e_v = (ulp_w() << n as isize) * (4 * terms + 8) * 3u32
214                + v.ulp().with_precision(0).value();
215            let radius = if minus_one {
216                // result = v_shifted − 1; the subtraction adds one result-ULP of rounding.
217                let result = &v_shifted - FBig::ONE;
218                let radius = (e_v << s) + result.ulp().with_precision(0).value();
219                return (result, radius);
220            } else {
221                e_v << s
222            };
223            (v_shifted, radius)
224        }
225    }
226}
227
228/// Hoisted `exp` overflow probe for the Ziv closures (which can't return `Err`). Returns `true`
229/// when `exp(x)` is outside the finite exponent range — astronomically large `|x|` (the reduction
230/// quotient `s = x/ln B` overflows `isize`). True for both signs of huge `x` (the quotient
231/// *magnitude* overflows `isize`). Shared by `exp_internal`, `powf`, and the hyperbolic functions.
232pub(crate) fn exp_overflows<R: Round, const B: Word>(
233    ctx: &Context<R>,
234    x: &Repr<B>,
235    cache: &mut Option<&mut ConstCache>,
236) -> bool {
237    if x.log2_est().abs() <= 61.0 {
238        return false;
239    }
240    let probe = Context::<R>::new(ctx.precision + 64);
241    let logb = probe.ln_base::<B>(reborrow_cache(cache));
242    let x_probe = FBig::new(probe.repr_round_ref(x).value(), probe);
243    let s_probe = x_probe.div_rem_euclid(logb).0;
244    <isize as core::convert::TryFrom<IBig>>::try_from(s_probe).is_err()
245}
246
247// `powi` (integer power), `powf` (non-integer exponent), `exp`, and `exp_m1` are correctly rounded
248// via the Ziv loop, so they require `R: ErrorBounds`. `powf` with an integer-valued exponent
249// delegates to `powi`.
250impl<R: ErrorBounds> Context<R> {
251    /// Raise the floating point number to an integer power under this context, correctly rounded
252    /// via a Ziv retry loop.
253    ///
254    /// `base^n` is computed by left-to-right binary exponentiation (repeated squaring); a negative
255    /// exponent computes `(1/base)^|n|`, so the sign-dependent overflow/underflow falls out
256    /// naturally. Each squaring compounds the relative error (it roughly doubles per step), so
257    /// after `n.bit_len()` squarings the error is bounded by about `2^nlen · ulp` — the Ziv radius
258    /// reflects that, and the loop retries with more guard digits until the working-precision
259    /// interval unambiguously determines the target rounding.
260    ///
261    /// # Examples
262    ///
263    /// ```
264    /// # use dashu_base::ParseError;
265    /// # use dashu_float::DBig;
266    /// # use core::str::FromStr;
267    /// use dashu_base::Approximation::*;
268    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
269    ///
270    /// let context = Context::<HalfAway>::new(2);
271    /// let a = DBig::from_str("-1.234")?;
272    /// assert_eq!(context.powi(&a.repr(), 10.into()), Ok(Inexact(DBig::from_str("8.2")?, AddOne)));
273    /// # Ok::<(), ParseError>(())
274    /// ```
275    ///
276    /// # Panics
277    ///
278    /// Panics if the precision is unlimited and the exponent is negative (the exact `1/base` is
279    /// not finite in general).
280    pub fn powi<const B: Word>(&self, base: &Repr<B>, exp: IBig) -> FpResult<FBig<R, B>> {
281        // TODO: range handling has three known limitations at the exponent extremes:
282        // (1) the overflow guard below estimates the result magnitude with an f64 and misclassifies
283        // representable boundaries near 2^63; (2) genuine Overflow/Underflow is unwrapped mode-blindly
284        // to ±inf / signed zero; (3) the negative-exponent reciprocal path can panic. None affects
285        // ordinary inputs; fixing requires mode-aware range saturation.
286        if base.is_infinite() {
287            return Err(FpError::InfiniteInput);
288        }
289        let (exp_sign, n) = exp.into_parts();
290        let negative = exp_sign == Sign::Negative;
291        if negative {
292            // a negative exponent needs 1/base, which is not finite at unlimited precision
293            assert_limited_precision(self.precision);
294        }
295
296        if n.is_zero() {
297            return Ok(Exact(FBig::ONE));
298        }
299        if n.is_one() {
300            if negative {
301                // base^(-1) = 1/base: a single correctly-rounded division
302                return self.div(&Repr::one(), base);
303            }
304            let repr = self.repr_round_ref(base);
305            return Ok(repr.map(|v| FBig::new(v, *self)));
306        }
307
308        // Zero base (±0): a positive exponent gives ±0, a negative one ±inf; the sign follows |n|'s
309        // parity. Short-circuit before the magnitude pre-check (whose log2 estimate is meaningless
310        // for zero) and before the squaring chain (which can't start from zero).
311        let odd = n.bit(0);
312        if base.significand.is_zero() {
313            let neg_sign = base.sign() == Sign::Negative && odd;
314            if negative {
315                let sign = if neg_sign {
316                    Sign::Negative
317                } else {
318                    Sign::Positive
319                };
320                return Ok(Exact(FBig::new(Repr::<B>::infinity_with_sign(sign), *self)));
321            }
322            let repr = if neg_sign {
323                Repr::<B>::neg_zero()
324            } else {
325                Repr::<B>::zero()
326            };
327            return Ok(Exact(FBig::new(repr, *self)));
328        }
329
330        // Magnitude pre-check: the result's log2 is `signed_exp · log2|base|`; outside the finite
331        // exponent range it short-circuits to overflow/underflow instead of letting the squaring
332        // chain overflow mid-computation (the Ziv closure below can't return `Err`).
333        //
334        // Use the *bounds* of log2(base), never the point estimate `log2_est`: when base is very
335        // close to 1 (a large significand with a large negative exponent), log2(base) is the
336        // difference of two large terms and suffers catastrophic cancellation — `log2_est` returns
337        // ~1e-4 of f32 noise rather than ~0. Scaled by a large exponent that noise crosses the
338        // overflow threshold, which on 32-bit is only isize::MAX·log2(B) ≈ 7e9 (vs ≈3e19 on 64-bit),
339        // so the guard fires spuriously and returns ±inf — see issue #95 (it crashed high-precision
340        // `FBig::with_base` on wasm32/i686). The bounds are derived from the exact significand bit
341        // length, so they don't cancel. Declare an extreme result only when a bound certifies it
342        // (no false positives); anything ambiguous is computed.
343        let (base_log2_lb, base_log2_ub) = base.log2_bounds();
344        let base_log2_lb = base_log2_lb as f64;
345        let base_log2_ub = base_log2_ub as f64;
346        let threshold = (isize::MAX as f64) * (B.log2_est() as f64);
347        let exp_f64 = i64::try_from(&n).ok().map(|e| e as f64);
348        // `lb_side` certifies |base| > 1 by a wide margin; `ub_side` certifies |base| < 1. (For the
349        // None case |exp| is unbounded, so the bound's sign alone decides.) A negative exponent
350        // swaps which side over- vs underflows.
351        let lb_side = match exp_f64 {
352            Some(e) => e * base_log2_lb > threshold,
353            None => base_log2_lb > 0.0,
354        };
355        let ub_side = match exp_f64 {
356            Some(e) => e * base_log2_ub < -threshold,
357            None => base_log2_ub < 0.0,
358        };
359        if lb_side || ub_side {
360            // |base|>1 (lb_side): positive exp → overflow, negative exp → underflow.
361            // |base|<1 (ub_side): positive exp → underflow, negative exp → overflow.
362            let overflow = (lb_side && !negative) || (ub_side && negative);
363            let sign = if base.sign() == Sign::Negative && odd {
364                Sign::Negative
365            } else {
366                Sign::Positive
367            };
368            return Err(if overflow {
369                FpError::Overflow(sign)
370            } else {
371                FpError::Underflow(sign)
372            });
373        }
374
375        // |exp| doesn't fit i64 and the bounds straddle 0, so |base| is within the bounds of 1.
376        // If it is *exactly* ±1 the result is ±1 for any exponent (short-circuit so the squaring
377        // chain doesn't iterate over exp's enormous bit length); otherwise |base| ≈ 1 but ≠ 1, the
378        // huge power is still finite, and we fall through to compute it.
379        if exp_f64.is_none() && base.significand.is_one() && base.exponent == 0 {
380            let repr = if base.sign() == Sign::Negative && odd {
381                Repr::<B>::neg_one()
382            } else {
383                Repr::<B>::one()
384            };
385            return Ok(Exact(FBig::new(repr, *self)));
386        }
387
388        let nlen = n.bit_len();
389        let initial_guard = nlen + self.base_guard_digits::<B>() + 2;
390        Ok(self.ziv(initial_guard, |guard| {
391            let pw = self.precision + guard;
392            let work = Context::<R>::new(pw);
393            // start from base (positive exponent, always exact) or its working-precision
394            // reciprocal (negative exponent, exact only when 1/base is exactly representable).
395            let (start, start_exact) = if negative {
396                let d = work.div(&Repr::one(), base);
397                let exact = matches!(d, Ok(Exact(_)));
398                (work.unwrap_fp(d).repr().clone(), exact)
399            } else {
400                (base.clone(), true)
401            };
402            let (res, chain_exact) = work.powi_chain(&start, &n);
403            // When the whole computation is exact (start exact + no squaring rounded), `res` is the
404            // exact value and the true error is 0 — report a zero radius. This is required under
405            // directed rounding modes, where an exactly-representable result lies on a one-sided
406            // rounding boundary that no nonzero radius can fit inside (the Ziv loop would retry
407            // forever). Otherwise the squaring compounds the error ~`2^nlen · ulp_w`.
408            let radius = if pw == 0 || (start_exact && chain_exact) {
409                FBig::ZERO
410            } else {
411                res.ulp().with_precision(0).value() << (nlen as isize + 1)
412            };
413            (res, radius)
414        }))
415    }
416
417    /// Raise the floating point number to an floating point power under this context.
418    ///
419    /// A non-integer exponent is correctly rounded via a Ziv loop. An integer-valued exponent
420    /// delegates to [`powi`](Context::powi) (binary exponentiation), which also accepts a negative
421    /// base — its sign is fixed by the exponent's parity — so `pow(-x, n)` is in domain here for
422    /// integer `n`. Both paths are correctly rounded.
423    ///
424    /// # Examples
425    ///
426    /// ```
427    /// # use dashu_base::ParseError;
428    /// # use dashu_float::DBig;
429    /// # use core::str::FromStr;
430    /// use dashu_base::Approximation::*;
431    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
432    ///
433    /// let context = Context::<HalfAway>::new(2);
434    /// let x = DBig::from_str("1.23")?;
435    /// let y = DBig::from_str("-4.56")?;
436    /// assert_eq!(context.powf(&x.repr(), &y.repr(), None), Ok(Inexact(DBig::from_str("0.39")?, AddOne)));
437    /// # Ok::<(), ParseError>(())
438    /// ```
439    ///
440    /// # Panics
441    ///
442    /// Panics if the precision is unlimited.
443    pub fn powf<const B: Word>(
444        &self,
445        base: &Repr<B>,
446        exp: &Repr<B>,
447        mut cache: Option<&mut ConstCache>,
448    ) -> FpResult<FBig<R, B>> {
449        if base.is_infinite() || exp.is_infinite() {
450            return Err(FpError::InfiniteInput);
451        }
452        assert_limited_precision(self.precision);
453
454        // shortcuts
455        if exp.is_pos_zero() || exp.is_neg_zero() {
456            // pow(x, ±0) = 1 for any base (IEEE 754 §9.2.1); `-0` is numerically zero, so it
457            // must take the same shortcut as `+0` (otherwise a negative base falls through to
458            // the OutOfDomain path below).
459            return Ok(Exact(FBig::ONE));
460        } else if exp.is_one() {
461            let repr = self.repr_round_ref(base);
462            return Ok(repr.map(|v| FBig::new(v, *self)));
463        } else if base.significand.is_zero() {
464            // With a *float* exponent the result on a zero base is the positive one — this
465            // matches the common float-pow convention (e.g. CPython: `(-0.0) ** y == 0.0`),
466            // which doesn't track the parity of the exponent:
467            //   pow(±0, y > 0) = +0,    pow(±0, y < 0) = +inf.
468            // For the sign-correct result (e.g. `pow(-0, odd) = -0`), use the integer-exponent
469            // [`powi`](Context::powi). Short-circuiting here also avoids the negative-base path.
470            return Ok(Exact(if exp.sign() == Sign::Negative {
471                FBig::new(Repr::infinity(), *self)
472            } else {
473                FBig::ZERO
474            }));
475        }
476        if base.is_one() {
477            // pow(1, y) = 1 for any finite y (exp is finite here — infinities were rejected above).
478            return Ok(Exact(FBig::ONE));
479        }
480
481        // Integer-valued exponent: delegate to the integer-power kernel (binary exponentiation),
482        // itself correctly rounded via its own Ziv loop. This sidesteps the `exp(y·ln x)`
483        // amplification entirely, and lets a negative base through — `powi` fixes the sign from
484        // the exponent's parity. Gated on `is_int` (a cheap exponent check) so the non-integer
485        // common case skips `to_int`.
486        if exp.is_int() {
487            return self.powi(base, exp.to_int().value());
488        }
489
490        if base.sign() == Sign::Negative {
491            // A non-integer exponent on a negative base has no real value.
492            return Err(FpError::OutOfDomain);
493        }
494
495        // x^y = exp(y·ln x), correctly rounded via the Ziv loop. `ln` and `exp` are themselves
496        // Ziv-correct at the working precision, so the radius comes only from the rounding of the
497        // `ln`/`mul`/`exp` chain — but `exp` AMPLIFIES the absolute error of its argument `y·ln x`
498        // by the result magnitude, i.e. by a relative factor of `|y·ln x|`. The radius is
499        // `result.ulp() · (|y·ln x| + 1) · (B + 8)` where `result.ulp()` is taken at the *working*
500        // precision, so it shrinks as `B^{-guard}` and the containment test converges. (A radius
501        // computed at unlimited precision would be constant across retries and never converge for
502        // a value near a rounding boundary.) The `B + 8` scale covers the `ulp`-vs-`value·B^{1-P}`
503        // gap plus a safety margin for the chained roundings.
504        //
505        // The overflow case is hoisted out of the Ziv closure (which can't return `Err`): if
506        // `exp(y·ln x)` falls outside the finite exponent range, short-circuit before the loop.
507        let probe = Context::<R>::new(self.precision + 32);
508        let ln_x_probe = probe.ln(base, reborrow_cache(&mut cache))?.value();
509        let arg_probe = probe.mul(ln_x_probe.repr(), exp)?.value();
510        if exp_overflows::<R, B>(&probe, arg_probe.repr(), &mut cache) {
511            return Err(if arg_probe.sign() == Sign::Positive {
512                FpError::Overflow(Sign::Positive)
513            } else {
514                FpError::Underflow(Sign::Positive)
515            });
516        }
517
518        let initial_guard = self.base_guard_digits::<B>() + 10;
519        Ok(self.ziv(initial_guard, |guard| {
520            let work = Context::<R>::new(self.precision + guard);
521            let ln_x = work.ln(base, reborrow_cache(&mut cache)).unwrap().value();
522            let arg = work.mul(ln_x.repr(), exp).unwrap().value();
523            let result = work
524                .exp(arg.repr(), reborrow_cache(&mut cache))
525                .unwrap()
526                .value();
527
528            // Radius at unlimited precision (exact arithmetic), but built from the *work-precision*
529            // `result.ulp()` so it carries the `B^{-(p+guard)}` scale and shrinks across retries.
530            let ulp_w = result.ulp().with_precision(0).value();
531            let arg_abs = arg.abs().with_precision(0).value();
532            let scale = (B as i32) + 8;
533            let radius = (ulp_w * (arg_abs + FBig::<R, B>::ONE)) * scale;
534            (result, radius)
535        }))
536    }
537
538    /// Calculate the exponential function (`eˣ`) on the floating point number under this context.
539    ///
540    /// # Examples
541    ///
542    /// ```
543    /// # use dashu_base::ParseError;
544    /// # use dashu_float::DBig;
545    /// # use core::str::FromStr;
546    /// use dashu_base::Approximation::*;
547    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
548    ///
549    /// let context = Context::<HalfAway>::new(2);
550    /// let a = DBig::from_str("-1.234")?;
551    /// assert_eq!(context.exp(&a.repr(), None), Ok(Inexact(DBig::from_str("0.29")?, NoOp)));
552    /// # Ok::<(), ParseError>(())
553    /// ```
554    #[inline]
555    pub fn exp<const B: Word>(
556        &self,
557        x: &Repr<B>,
558        cache: Option<&mut ConstCache>,
559    ) -> FpResult<FBig<R, B>> {
560        if x.is_infinite() {
561            return Ok(Exact(FBig::new(
562                match x.sign() {
563                    Sign::Positive => Repr::infinity(),
564                    Sign::Negative => Repr::zero(),
565                },
566                *self,
567            )));
568        }
569        self.exp_internal(x, false, cache)
570    }
571
572    /// Calculate the exponential minus one function (`eˣ-1`) on the floating point number under this context.
573    ///
574    /// # Examples
575    ///
576    /// ```
577    /// # use dashu_base::ParseError;
578    /// # use dashu_float::DBig;
579    /// # use core::str::FromStr;
580    /// use dashu_base::Approximation::*;
581    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
582    ///
583    /// let context = Context::<HalfAway>::new(2);
584    /// let a = DBig::from_str("-0.1234")?;
585    /// assert_eq!(context.exp_m1(&a.repr(), None), Ok(Inexact(DBig::from_str("-0.12")?, SubOne)));
586    /// # Ok::<(), ParseError>(())
587    /// ```
588    #[inline]
589    pub fn exp_m1<const B: Word>(
590        &self,
591        x: &Repr<B>,
592        cache: Option<&mut ConstCache>,
593    ) -> FpResult<FBig<R, B>> {
594        if x.is_infinite() {
595            return match x.sign() {
596                Sign::Positive => Ok(Exact(FBig::new(Repr::infinity(), *self))),
597                Sign::Negative => Ok(Exact(-FBig::ONE)), // exp_m1(−∞) = −1
598            };
599        }
600        self.exp_internal(x, true, cache)
601    }
602
603    // TODO: change reduction to (x - s log2) / 2ⁿ, so that the final powering is always base 2, and doesn't depends on powi.
604    //       the powering exp(r)^(2ⁿ) could be optimized by noticing (1+x)^2 - 1 = x^2 + 2x
605    //       consider this change after having a benchmark
606
607    fn exp_internal<const B: Word>(
608        &self,
609        x: &Repr<B>,
610        minus_one: bool,
611        mut cache: Option<&mut ConstCache>,
612    ) -> FpResult<FBig<R, B>> {
613        assert_finite(x);
614        let input_sign = x.sign();
615
616        if x.significand.is_zero() {
617            // exp(±0) = 1; exp_m1(±0) = ±0 (IEEE 754 §9.2.1 preserves the sign of zero).
618            // These exact results need no rounding, so handle them before the
619            // limited-precision assertion: a precision-0 (unlimited) FBig such as the
620            // one produced by `try_from(0.0)` must still compute exp/exp_m1 exactly.
621            return match minus_one {
622                false => Ok(Exact(FBig::ONE)),
623                true => {
624                    let zero = if input_sign == Sign::Negative {
625                        FBig::new(Repr::neg_zero(), Context::new(0))
626                    } else {
627                        FBig::ZERO
628                    };
629                    Ok(Exact(zero))
630                }
631            };
632        }
633
634        assert_limited_precision(self.precision);
635
636        // Hoisted overflow check: the reduction quotient s = floor(x/ln B) overflows isize only
637        // for astronomically large |x| (|x| ≳ 2^61). The Ziv closure below can't return Err, so
638        // detect that case here and short-circuit to overflow/underflow (matching IEEE limits).
639        if x.log2_est().abs() > 61.0 {
640            let probe = Context::<R>::new(self.precision + 64);
641            let logb = probe.ln_base::<B>(reborrow_cache(&mut cache));
642            let x_probe = FBig::new(probe.repr_round_ref(x).value(), probe);
643            let s_probe = x_probe.div_rem_euclid(logb).0;
644            if <isize as core::convert::TryFrom<IBig>>::try_from(s_probe).is_err() {
645                return if input_sign == Sign::Positive {
646                    Err(FpError::Overflow(Sign::Positive))
647                } else if minus_one {
648                    Ok(Exact(-FBig::ONE)) // exp_m1(−∞) = −1 (finite)
649                } else {
650                    Err(FpError::Underflow(Sign::Positive)) // exp(−∞) = +0
651                };
652            }
653        }
654
655        // Correct rounding via the Ziv loop. Guards: log_B(p) for the series summation/squaring
656        // rounding, plus `n` for the Bⁿ powering amplification — halved from the pre-Ziv `2n`,
657        // since Ziv (not the guard count) now certifies correctness. `n ≈ √p` is derived from the
658        // target precision and is constant across retries.
659        let series_guard = self.base_guard_digits::<B>();
660        let n = 1usize << (self.precision.bit_len() / 2);
661        Ok(self.ziv(series_guard + n, |guard| {
662            self.exp_compute::<B>(
663                x,
664                self.precision + guard,
665                minus_one,
666                n,
667                reborrow_cache(&mut cache),
668            )
669        }))
670    }
671}
672
673#[cfg(test)]
674mod tests {
675    use super::*;
676    use crate::round::mode;
677
678    #[test]
679    fn test_exp_overflow_is_infinity() {
680        let ctx = Context::<mode::HalfEven>::new(53);
681        // exp(huge) overflows the isize exponent range -> Overflow at Context level.
682        // Need x large enough that floor(x/ln2) > isize::MAX, i.e. x > ~2^62.5.
683        let huge = Repr::new(IBig::from(1) << 63, 0);
684        assert_eq!(ctx.exp::<2>(&huge, None), Err(FpError::Overflow(Sign::Positive)));
685
686        // exp(huge negative) underflows to +0
687        let neg = Repr::new(-(IBig::from(1) << 63), 0);
688        assert_eq!(ctx.exp::<2>(&neg, None), Err(FpError::Underflow(Sign::Positive)));
689
690        // exp_m1(huge negative) -> -1 (a finite value, not an error)
691        let m1 = ctx.exp_m1::<2>(&neg, None).unwrap().value();
692        assert_eq!(m1, -FBig::<mode::HalfEven>::ONE);
693    }
694
695    // A sharp OOM regression needs an exponent gap large enough that 2^gap exceeds any
696    // memory (gap ≳ 1e11), yet with floor(x/ln2) still fitting isize so the overflow
697    // branch is not taken. That window only exists where isize is 64-bit: on 32-bit,
698    // isize tops out at ~2.1e9 — below any OOM-inducing gap — so the overflow branch
699    // always intervenes first. The fix itself (log2_bounds in round_fract) is
700    // arch-independent; only this dedicated sharp test is 64-bit-only.
701    #[test]
702    fn test_exact_results_on_unlimited_precision() {
703        // Regression test: values carrying precision 0 (unlimited) — produced by
704        // `try_from(0.0)` and the `FBig::ONE`/`ZERO` constants — must still compute
705        // their exact-result special cases instead of panicking in
706        // assert_limited_precision before reaching the shortcut.
707        type F = FBig<mode::HalfEven, 2>;
708
709        let zero = F::try_from(0.0_f64).unwrap();
710        assert_eq!(zero.exp(), F::ONE);
711        assert_eq!(zero.exp_m1(), F::ZERO);
712        assert_eq!(zero.sqrt(), F::ZERO);
713        assert_eq!(zero.ln_1p(), F::ZERO);
714
715        // -0.0 preserves its sign through exp_m1 and sqrt.
716        let neg_zero = F::try_from(-0.0_f64).unwrap();
717        assert!(neg_zero.exp_m1().repr().is_neg_zero());
718        assert!(neg_zero.sqrt().repr().is_neg_zero());
719
720        // FBig::ONE carries unlimited precision; ln(1) = 0 is exact.
721        assert_eq!(F::ONE.ln(), F::ZERO);
722    }
723
724    #[test]
725    fn test_powf_zero_base() {
726        use crate::DBig;
727        // powf with a float exponent returns the *positive* result on a zero base
728        // (matching the common float-pow convention); use powi for the signed result.
729        let ctx = Context::<mode::HalfEven>::new(53);
730        // powf(-0, 2.0) = +0 (NOT -0)
731        let r = ctx
732            .powf::<2>(&Repr::<2>::neg_zero(), &Repr::new(2.into(), 0), None)
733            .unwrap()
734            .value();
735        assert!(r.repr().is_pos_zero(), "expected +0");
736        assert!(!r.repr().is_neg_zero(), "powf(-0, x) should be +0, not -0");
737        // powf(0, -1) = +inf
738        let r = ctx
739            .powf::<2>(&Repr::<2>::zero(), &Repr::new((-1i32).into(), 0), None)
740            .unwrap()
741            .value();
742        assert!(r.repr().is_infinite());
743        assert_eq!(r.repr().sign(), Sign::Positive);
744        // powi(-0, 3) = -0 (the sign-correct, integer-exponent variant)
745        let r = ctx
746            .powi::<2>(&Repr::<2>::neg_zero(), 3.into())
747            .unwrap()
748            .value();
749        assert!(r.repr().is_neg_zero());
750        let _ = DBig::ZERO;
751    }
752
753    #[test]
754    fn test_powf_integer_exponent() {
755        use crate::DBig;
756        let ctx = Context::<mode::HalfEven>::new(53);
757        // integer-valued float exponent delegates to powi and supports a negative base (its sign
758        // is fixed by the exponent's parity): (-5)^3 = -125.
759        let neg_base = &Repr::<2>::new((-5).into(), 0);
760        let exp3 = &Repr::<2>::new(3.into(), 0);
761        let via_powf = ctx.powf::<2>(neg_base, exp3, None).unwrap().value();
762        let via_powi = ctx.powi::<2>(neg_base, 3.into()).unwrap().value();
763        assert_eq!(via_powf.repr(), via_powi.repr());
764        assert_eq!(via_powf.repr().sign(), Sign::Negative);
765
766        // a non-integer exponent on a negative base is out of domain (no real value)
767        let exp_half = &Repr::<2>::new(5.into(), -1); // 2.5
768        assert_eq!(ctx.powf::<2>(neg_base, exp_half, None), Err(FpError::OutOfDomain));
769
770        // positive base, integer exponent: also routes through powi
771        let pos_base = &Repr::<2>::new(3.into(), 0);
772        let exp4 = &Repr::<2>::new(4.into(), 0);
773        let r = ctx.powf::<2>(pos_base, exp4, None).unwrap().value();
774        assert_eq!(r.repr(), ctx.powi::<2>(pos_base, 4.into()).unwrap().value().repr());
775        let _ = DBig::ZERO;
776    }
777}