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}