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::Round,
9    utils::ceil_usize,
10};
11use dashu_base::{AbsOrd, Approximation::*, BitTest, DivRemEuclid, EstimatedLog2, Sign};
12use dashu_int::IBig;
13
14impl<R: Round, const B: Word> FBig<R, B> {
15    /// Raise the floating point number to an integer power.
16    ///
17    /// # Examples
18    ///
19    /// ```
20    /// # use dashu_base::ParseError;
21    /// # use dashu_float::DBig;
22    /// # use core::str::FromStr;
23    /// let a = DBig::from_str("-1.234")?;
24    /// assert_eq!(a.powi(10.into()), DBig::from_str("8.188")?);
25    /// # Ok::<(), ParseError>(())
26    /// ```
27    #[inline]
28    pub fn powi(&self, exp: IBig) -> FBig<R, B> {
29        self.context.unwrap_fp(self.context.powi(&self.repr, exp))
30    }
31
32    /// Raise the floating point number to an floating point power.
33    ///
34    /// # Examples
35    ///
36    /// ```
37    /// # use dashu_base::ParseError;
38    /// # use dashu_float::DBig;
39    /// # use core::str::FromStr;
40    /// let x = DBig::from_str("1.23")?;
41    /// let y = DBig::from_str("-4.56")?;
42    /// assert_eq!(x.powf(&y), DBig::from_str("0.389")?);
43    /// # Ok::<(), ParseError>(())
44    /// ```
45    #[inline]
46    pub fn powf(&self, exp: &Self) -> Self {
47        let context = Context::max(self.context, exp.context);
48        context.unwrap_fp(context.powf(&self.repr, &exp.repr, None))
49    }
50
51    /// Calculate the exponential function (`eˣ`) on the floating point number.
52    ///
53    /// # Examples
54    ///
55    /// ```
56    /// # use dashu_base::ParseError;
57    /// # use dashu_float::DBig;
58    /// # use core::str::FromStr;
59    /// let a = DBig::from_str("-1.234")?;
60    /// assert_eq!(a.exp(), DBig::from_str("0.2911")?);
61    /// # Ok::<(), ParseError>(())
62    /// ```
63    #[inline]
64    pub fn exp(&self) -> FBig<R, B> {
65        self.context.unwrap_fp(self.context.exp(&self.repr, None))
66    }
67
68    /// Calculate the exponential minus one function (`eˣ-1`) on the floating point number.
69    ///
70    /// # Examples
71    ///
72    /// ```
73    /// # use dashu_base::ParseError;
74    /// # use dashu_float::DBig;
75    /// # use core::str::FromStr;
76    /// let a = DBig::from_str("-0.1234")?;
77    /// assert_eq!(a.exp_m1(), DBig::from_str("-0.11609")?);
78    /// # Ok::<(), ParseError>(())
79    /// ```
80    #[inline]
81    pub fn exp_m1(&self) -> FBig<R, B> {
82        self.context
83            .unwrap_fp(self.context.exp_m1(&self.repr, None))
84    }
85}
86
87// TODO: give the exact formulation of required guard bits
88
89impl<R: Round> Context<R> {
90    /// Raise the floating point number to an integer power under this context.
91    ///
92    /// # Examples
93    ///
94    /// ```
95    /// # use dashu_base::ParseError;
96    /// # use dashu_float::DBig;
97    /// # use core::str::FromStr;
98    /// use dashu_base::Approximation::*;
99    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
100    ///
101    /// let context = Context::<HalfAway>::new(2);
102    /// let a = DBig::from_str("-1.234")?;
103    /// assert_eq!(context.powi(&a.repr(), 10.into()), Ok(Inexact(DBig::from_str("8.2")?, AddOne)));
104    /// # Ok::<(), ParseError>(())
105    /// ```
106    ///
107    /// # Panics
108    ///
109    /// Panics if the precision is unlimited and the exponent is negative. In this case, the exact
110    /// result is likely to have infinite digits.
111    pub fn powi<const B: Word>(&self, base: &Repr<B>, exp: IBig) -> FpResult<FBig<R, B>> {
112        // TODO: range handling has three known limitations at the exponent extremes:
113        // (1) the overflow guard below estimates the result magnitude with an f64 and misclassifies
114        // representable boundaries near 2^63; (2) genuine Overflow/Underflow is unwrapped mode-blindly
115        // to ±inf / signed zero; (3) the negative-exponent reciprocal path can panic. None affects
116        // ordinary inputs; fixing requires mode-aware range saturation.
117        if base.is_infinite() {
118            return Err(FpError::InfiniteInput);
119        }
120
121        let (exp_sign, exp) = exp.into_parts();
122        if exp_sign == Sign::Negative {
123            // if the exponent is negative, then negate the exponent
124            // note that do the inverse at last requires less guard bits
125            assert_limited_precision(self.precision); // TODO: we can allow this if the inverse is exact (only when significand is one?)
126
127            let guard_bits = self.precision.bit_len() * 2; // heuristic
128            let rev_context = Context::<R::Reverse>::new(self.precision + guard_bits);
129            let pow = rev_context.unwrap_fp(rev_context.powi(base, exp.into()));
130            let inv = rev_context.unwrap_fp_repr(rev_context.repr_div(Repr::one(), pow.repr));
131            let repr = self.repr_round(inv);
132            return Ok(repr.map(|v| FBig::new(v, *self)));
133        }
134        if exp.is_zero() {
135            return Ok(Exact(FBig::ONE));
136        } else if exp.is_one() {
137            let repr = self.repr_round_ref(base);
138            return Ok(repr.map(|v| FBig::new(v, *self)));
139        }
140
141        // Guard against exponent overflow/underflow for astronomically large results.
142        // The result magnitude has log2 ≈ exp·log2(base); if that leaves the isize
143        // exponent range, return ±inf (|base|>1) or signed 0 (|base|<1) instead of
144        // overflowing the exponent mid-computation.
145        //
146        // Use the *bounds* of log2(base), never the point estimate `log2_est`: when
147        // base is very close to 1 (a large significand with a large negative exponent),
148        // log2(base) is the difference of two ~1e3-magnitude terms and suffers
149        // catastrophic cancellation — `log2_est` returns ~1e-4 of f32 noise rather than
150        // ~0. Scaled by a large exponent that noise crosses the overflow threshold,
151        // which on 32-bit is only isize::MAX·log2(B) ≈ 7e9 (vs ≈3e19 on 64-bit), so the
152        // guard fires spuriously and returns ±inf — see issue #95. The bounds are
153        // derived from the exact significand bit length, so they don't cancel. Declare
154        // overflow only on the lower bound (no false positives) and underflow only on
155        // the upper bound (no false underflows); anything in between is computed.
156        let (base_log2_lb, base_log2_ub) = base.log2_bounds();
157        let base_log2_lb = base_log2_lb as f64;
158        let base_log2_ub = base_log2_ub as f64;
159        let threshold = (isize::MAX as f64) * (B.log2_est() as f64);
160        let exp_f64 = i64::try_from(&exp).ok().map(|e| e as f64);
161        let overflows = match exp_f64 {
162            Some(e) => e * base_log2_lb > threshold,
163            None => base_log2_lb > 0.0, // exp doesn't fit i64: overflows iff base > 1
164        };
165        if overflows {
166            return Err(FpError::Overflow(if base.sign() == Sign::Negative {
167                Sign::Negative
168            } else {
169                Sign::Positive
170            }));
171        }
172        let underflows = !base.significand.is_zero()
173            && match exp_f64 {
174                Some(e) => e * base_log2_ub < -threshold,
175                None => base_log2_ub < 0.0, // exp doesn't fit i64: underflows iff base < 1
176            };
177        if underflows {
178            // |base| < 1 and exponent huge → underflow to signed zero
179            let underflow_sign = if base.sign() == Sign::Negative && exp.bit(0) {
180                Sign::Negative
181            } else {
182                Sign::Positive
183            };
184            return Err(FpError::Underflow(underflow_sign));
185        }
186
187        let work_context = if self.is_limited() {
188            // increase working precision when the exponent is large
189            let guard_digits = exp.bit_len() + self.precision.bit_len(); // heuristic
190            Context::<R>::new(self.precision + guard_digits)
191        } else {
192            Context::<R>::new(0)
193        };
194
195        // binary exponentiation from left to right
196        let mut p = exp.bit_len() - 2;
197        let mut res = work_context.unwrap_fp(work_context.sqr(base));
198        loop {
199            if exp.bit(p) {
200                res = work_context.unwrap_fp(work_context.mul(res.repr(), base));
201            }
202            if p == 0 {
203                break;
204            }
205            p -= 1;
206            res = work_context.unwrap_fp(work_context.sqr(res.repr()));
207        }
208
209        Ok(res.with_precision(self.precision))
210    }
211
212    /// Raise the floating point number to an floating point power under this context.
213    ///
214    /// Note that this method will not rely on [FBig::powi] even if the `exp` is actually an integer.
215    ///
216    /// # Examples
217    ///
218    /// ```
219    /// # use dashu_base::ParseError;
220    /// # use dashu_float::DBig;
221    /// # use core::str::FromStr;
222    /// use dashu_base::Approximation::*;
223    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
224    ///
225    /// let context = Context::<HalfAway>::new(2);
226    /// let x = DBig::from_str("1.23")?;
227    /// let y = DBig::from_str("-4.56")?;
228    /// assert_eq!(context.powf(&x.repr(), &y.repr(), None), Ok(Inexact(DBig::from_str("0.39")?, AddOne)));
229    /// # Ok::<(), ParseError>(())
230    /// ```
231    ///
232    /// # Panics
233    ///
234    /// Panics if the precision is unlimited.
235    pub fn powf<const B: Word>(
236        &self,
237        base: &Repr<B>,
238        exp: &Repr<B>,
239        mut cache: Option<&mut ConstCache>,
240    ) -> FpResult<FBig<R, B>> {
241        if base.is_infinite() || exp.is_infinite() {
242            return Err(FpError::InfiniteInput);
243        }
244        assert_limited_precision(self.precision); // TODO: we can allow it if exp is integer
245
246        // shortcuts
247        if exp.is_pos_zero() || exp.is_neg_zero() {
248            // pow(x, ±0) = 1 for any base (IEEE 754 §9.2.1); `-0` is numerically zero, so it
249            // must take the same shortcut as `+0` (otherwise a negative base falls through to
250            // the OutOfDomain path below).
251            return Ok(Exact(FBig::ONE));
252        } else if exp.is_one() {
253            let repr = self.repr_round_ref(base);
254            return Ok(repr.map(|v| FBig::new(v, *self)));
255        } else if base.significand.is_zero() {
256            // With a *float* exponent the result on a zero base is the positive one — this
257            // matches the common float-pow convention (e.g. CPython: `(-0.0) ** y == 0.0`),
258            // which doesn't track the parity of the exponent:
259            //   pow(±0, y > 0) = +0,    pow(±0, y < 0) = +inf.
260            // For the sign-correct result (e.g. `pow(-0, odd) = -0`), use the integer-exponent
261            // [`powi`](Context::powi). Short-circuiting here also avoids the negative-base path.
262            return Ok(Exact(if exp.sign() == Sign::Negative {
263                FBig::new(Repr::infinity(), *self)
264            } else {
265                FBig::ZERO
266            }));
267        }
268        if base.sign() == Sign::Negative {
269            // TODO: we should allow negative base when exp is an integer
270            return Err(FpError::OutOfDomain);
271        }
272
273        // x^y = exp(y*ln(x)), use a simple rule for guard bits
274        let guard_digits = 10 + ceil_usize(self.precision.log2_est());
275        let work_context = Context::<R>::new(self.precision + guard_digits);
276
277        // ln and exp each consult/extend the shared cache; reborrows are sequential.
278        let ln_val = work_context.unwrap_fp(work_context.ln(base, reborrow_cache(&mut cache)));
279        let mul_val = work_context.unwrap_fp(work_context.mul(ln_val.repr(), exp));
280        let exp_val =
281            work_context.unwrap_fp(work_context.exp(mul_val.repr(), reborrow_cache(&mut cache)));
282        Ok(exp_val.with_precision(self.precision))
283    }
284
285    /// Calculate the exponential function (`eˣ`) on the floating point number under this context.
286    ///
287    /// # Examples
288    ///
289    /// ```
290    /// # use dashu_base::ParseError;
291    /// # use dashu_float::DBig;
292    /// # use core::str::FromStr;
293    /// use dashu_base::Approximation::*;
294    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
295    ///
296    /// let context = Context::<HalfAway>::new(2);
297    /// let a = DBig::from_str("-1.234")?;
298    /// assert_eq!(context.exp(&a.repr(), None), Ok(Inexact(DBig::from_str("0.29")?, NoOp)));
299    /// # Ok::<(), ParseError>(())
300    /// ```
301    #[inline]
302    pub fn exp<const B: Word>(
303        &self,
304        x: &Repr<B>,
305        cache: Option<&mut ConstCache>,
306    ) -> FpResult<FBig<R, B>> {
307        if x.is_infinite() {
308            return Ok(Exact(FBig::new(
309                match x.sign() {
310                    Sign::Positive => Repr::infinity(),
311                    Sign::Negative => Repr::zero(),
312                },
313                *self,
314            )));
315        }
316        self.exp_internal(x, false, cache)
317    }
318
319    /// Calculate the exponential minus one function (`eˣ-1`) on the floating point number under this context.
320    ///
321    /// # Examples
322    ///
323    /// ```
324    /// # use dashu_base::ParseError;
325    /// # use dashu_float::DBig;
326    /// # use core::str::FromStr;
327    /// use dashu_base::Approximation::*;
328    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
329    ///
330    /// let context = Context::<HalfAway>::new(2);
331    /// let a = DBig::from_str("-0.1234")?;
332    /// assert_eq!(context.exp_m1(&a.repr(), None), Ok(Inexact(DBig::from_str("-0.12")?, SubOne)));
333    /// # Ok::<(), ParseError>(())
334    /// ```
335    #[inline]
336    pub fn exp_m1<const B: Word>(
337        &self,
338        x: &Repr<B>,
339        cache: Option<&mut ConstCache>,
340    ) -> FpResult<FBig<R, B>> {
341        if x.is_infinite() {
342            return match x.sign() {
343                Sign::Positive => Ok(Exact(FBig::new(Repr::infinity(), *self))),
344                Sign::Negative => Ok(Exact(-FBig::ONE)), // exp_m1(−∞) = −1
345            };
346        }
347        self.exp_internal(x, true, cache)
348    }
349
350    // TODO: change reduction to (x - s log2) / 2ⁿ, so that the final powering is always base 2, and doesn't depends on powi.
351    //       the powering exp(r)^(2ⁿ) could be optimized by noticing (1+x)^2 - 1 = x^2 + 2x
352    //       consider this change after having a benchmark
353
354    fn exp_internal<const B: Word>(
355        &self,
356        x: &Repr<B>,
357        minus_one: bool,
358        mut cache: Option<&mut ConstCache>,
359    ) -> FpResult<FBig<R, B>> {
360        assert_finite(x);
361        let input_sign = x.sign();
362
363        if x.significand.is_zero() {
364            // exp(±0) = 1; exp_m1(±0) = ±0 (IEEE 754 §9.2.1 preserves the sign of zero).
365            // These exact results need no rounding, so handle them before the
366            // limited-precision assertion: a precision-0 (unlimited) FBig such as the
367            // one produced by `try_from(0.0)` must still compute exp/exp_m1 exactly.
368            return match minus_one {
369                false => Ok(Exact(FBig::ONE)),
370                true => {
371                    let zero = if input_sign == Sign::Negative {
372                        FBig::new(Repr::neg_zero(), Context::new(0))
373                    } else {
374                        FBig::ZERO
375                    };
376                    Ok(Exact(zero))
377                }
378            };
379        }
380
381        assert_limited_precision(self.precision);
382
383        // A simple algorithm:
384        // - let r = (x - s logB) / Bⁿ, where s = floor(x / logB), such that r < B⁻ⁿ.
385        // - if the target precision is p digits, then there're only about p/m terms in Tyler series
386        // - finally, exp(x) = Bˢ * exp(r)^(Bⁿ)
387        // - the optimal n is √p as given by MPFR
388
389        // Maclaurin series: exp(r) = 1 + Σ(rⁱ/i!)
390        // There will be about p/log_B(r) summations when calculating the series, to prevent
391        // loss of significance, we need about log_B(p) guard digits.
392        let series_guard_digits = ceil_usize(self.precision.log2_est() / B.log2_est()) + 2;
393
394        // Reduction power: the series value is later raised to Bⁿ, which amplifies its
395        // relative error by a factor of Bⁿ. So the series (and the squarings) must carry
396        // about n extra base-B digits for the result to come out correct to p digits. We
397        // use 2n for safety — this mirrors MPFR's working precision q = precy + 2·K + …
398        // (K ≈ √precy is MPFR's squaring count, the analogue of our n). The log_B(p)
399        // summation/squaring rounding terms are already covered by series_guard_digits.
400        let n = 1usize << (self.precision.bit_len() / 2);
401        let pow_guard_digits = 2 * n;
402        let work_precision;
403
404        // When minus_one is true and |x| < 1/B, the input is fed into the Maclaurin series without scaling
405        let no_scaling = minus_one && x.log2_est() < -B.log2_est();
406        let (s, n, r) = if no_scaling {
407            // if minus_one is true and x is already small (x < 1/B),
408            // then directly evaluate the Maclaurin series without scaling
409            if x.sign() == Sign::Negative {
410                // extra digits are required to prevent cancellation during the summation
411                work_precision = self.precision + 2 * series_guard_digits;
412            } else {
413                work_precision = self.precision + series_guard_digits;
414            }
415            let context = Context::<R>::new(work_precision);
416            (0, 0, FBig::new(context.repr_round_ref(x).value(), context))
417        } else {
418            work_precision = self.precision + series_guard_digits + pow_guard_digits;
419            let context = Context::<R>::new(work_precision);
420            let x = FBig::new(context.repr_round_ref(x).value(), context);
421            let logb = context.ln_base::<B>(reborrow_cache(&mut cache));
422            let (s, r) = x.div_rem_euclid(logb);
423
424            let s: isize = match s.try_into() {
425                Ok(v) => v,
426                Err(_) => {
427                    // |floor(x / ln B)| overflows isize — x is astronomically large, so the
428                    // result is an infinity (x → +∞) or underflows to the limit (x → −∞).
429                    //
430                    // TODO: this branch discards the rounding mode. For a huge
431                    // *negative* x the true exp(x) is positive but below the exponent range, so
432                    // directed Up/Away should return the minimum positive value (and exp_m1 the
433                    // value just above -1) rather than +0 / Exact(-1).
434                    return if input_sign == Sign::Positive {
435                        Err(FpError::Overflow(Sign::Positive))
436                    } else if minus_one {
437                        Ok(Exact(-FBig::ONE)) // exp_m1(−∞) = −1 (finite)
438                    } else {
439                        Err(FpError::Underflow(Sign::Positive)) // exp(−∞) = +0
440                    };
441                }
442            };
443            (s, n, r)
444        };
445
446        let r = r >> n as isize;
447        let mut factorial = IBig::ONE;
448        let mut pow = r.clone();
449        let mut sum = if no_scaling {
450            r.clone()
451        } else {
452            FBig::ONE + &r
453        };
454
455        let mut k = 2;
456        loop {
457            factorial *= k;
458            pow *= &r;
459
460            let increase = &pow / &factorial;
461            if increase.abs_cmp(&sum.sub_ulp()).is_le() {
462                break;
463            }
464            sum += increase;
465            k += 1;
466        }
467
468        if no_scaling {
469            Ok(sum.with_precision(self.precision))
470        } else if minus_one {
471            // Power at the series' working precision (it already carries the 2n guard
472            // digits that the Bⁿ powering amplifies away). The final "−1" can cancel at
473            // most ~1 leading digit here (the |x| < 1/B case is handled by no_scaling),
474            // which the same guard digits comfortably absorb.
475            let pow_ctx = Context::<R>::new(work_precision);
476            let v = pow_ctx.unwrap_fp(pow_ctx.powi(sum.repr(), Repr::<B>::BASE.pow(n).into()));
477            Ok(((v << s) - FBig::ONE).with_precision(self.precision))
478        } else {
479            let pow_ctx = Context::<R>::new(work_precision);
480            let v = pow_ctx.unwrap_fp(pow_ctx.powi(sum.repr(), Repr::<B>::BASE.pow(n).into()));
481            Ok((v << s).with_precision(self.precision))
482        }
483    }
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489    use crate::round::mode;
490
491    #[test]
492    fn test_exp_overflow_is_infinity() {
493        let ctx = Context::<mode::HalfEven>::new(53);
494        // exp(huge) overflows the isize exponent range -> Overflow at Context level.
495        // Need x large enough that floor(x/ln2) > isize::MAX, i.e. x > ~2^62.5.
496        let huge = Repr::new(IBig::from(1) << 63, 0);
497        assert_eq!(ctx.exp::<2>(&huge, None), Err(FpError::Overflow(Sign::Positive)));
498
499        // exp(huge negative) underflows to +0
500        let neg = Repr::new(-(IBig::from(1) << 63), 0);
501        assert_eq!(ctx.exp::<2>(&neg, None), Err(FpError::Underflow(Sign::Positive)));
502
503        // exp_m1(huge negative) -> -1 (a finite value, not an error)
504        let m1 = ctx.exp_m1::<2>(&neg, None).unwrap().value();
505        assert_eq!(m1, -FBig::<mode::HalfEven>::ONE);
506    }
507
508    // A sharp OOM regression needs an exponent gap large enough that 2^gap exceeds any
509    // memory (gap ≳ 1e11), yet with floor(x/ln2) still fitting isize so the overflow
510    // branch is not taken. That window only exists where isize is 64-bit: on 32-bit,
511    // isize tops out at ~2.1e9 — below any OOM-inducing gap — so the overflow branch
512    // always intervenes first. The fix itself (log2_bounds in round_fract) is
513    // arch-independent; only this dedicated sharp test is 64-bit-only.
514    #[cfg(target_pointer_width = "64")]
515    #[test]
516    fn test_exp_m1_large_negative_no_oom() {
517        // Regression test: exp_m1 of a large-magnitude negative input subtracts
518        // 1 from an astronomically small exp(x), so the aligned subtraction hands
519        // round_fract a sparse sticky tail whose `precision` equals the exponent gap
520        // (~6.6e18 here). The debug assert in round_fract used to materialize B^precision
521        // (2^6.6e18 bits → certain OOM); it now uses log2 bounds and must not allocate.
522        //
523        // x = -2^62: the gap (~6.6e18) dwarfs memory, yet floor(x/ln2) ≈ -6.6e18 still
524        // fits 64-bit isize (≈9.2e18), so this does NOT enter the overflow branch.
525        let ctx = Context::<mode::Up>::new(2);
526        let x = Repr::new(-(IBig::from(1) << 62), 0);
527        let r = ctx.exp_m1::<2>(&x, None).unwrap().value();
528        // exp_m1(-2^62) = -1 + ε (ε > 0 vanishingly small); under Up it rounds above -1.
529        assert!(
530            r > -FBig::<mode::Up, 2>::ONE,
531            "exp_m1(-2^62) under Up should round above -1, got {r:?}"
532        );
533    }
534
535    #[test]
536    fn test_exact_results_on_unlimited_precision() {
537        // Regression test: values carrying precision 0 (unlimited) — produced by
538        // `try_from(0.0)` and the `FBig::ONE`/`ZERO` constants — must still compute
539        // their exact-result special cases instead of panicking in
540        // assert_limited_precision before reaching the shortcut.
541        type F = FBig<mode::HalfEven, 2>;
542
543        let zero = F::try_from(0.0_f64).unwrap();
544        assert_eq!(zero.exp(), F::ONE);
545        assert_eq!(zero.exp_m1(), F::ZERO);
546        assert_eq!(zero.sqrt(), F::ZERO);
547        assert_eq!(zero.ln_1p(), F::ZERO);
548
549        // -0.0 preserves its sign through exp_m1 and sqrt.
550        let neg_zero = F::try_from(-0.0_f64).unwrap();
551        assert!(neg_zero.exp_m1().repr().is_neg_zero());
552        assert!(neg_zero.sqrt().repr().is_neg_zero());
553
554        // FBig::ONE carries unlimited precision; ln(1) = 0 is exact.
555        assert_eq!(F::ONE.ln(), F::ZERO);
556    }
557
558    #[test]
559    fn test_powf_zero_base() {
560        use crate::DBig;
561        // powf with a float exponent returns the *positive* result on a zero base
562        // (matching the common float-pow convention); use powi for the signed result.
563        let ctx = Context::<mode::HalfEven>::new(53);
564        // powf(-0, 2.0) = +0 (NOT -0)
565        let r = ctx
566            .powf::<2>(&Repr::<2>::neg_zero(), &Repr::new(2.into(), 0), None)
567            .unwrap()
568            .value();
569        assert!(r.repr().is_pos_zero(), "expected +0");
570        assert!(!r.repr().is_neg_zero(), "powf(-0, x) should be +0, not -0");
571        // powf(0, -1) = +inf
572        let r = ctx
573            .powf::<2>(&Repr::<2>::zero(), &Repr::new((-1i32).into(), 0), None)
574            .unwrap()
575            .value();
576        assert!(r.repr().is_infinite());
577        assert_eq!(r.repr().sign(), Sign::Positive);
578        // powi(-0, 3) = -0 (the sign-correct, integer-exponent variant)
579        let r = ctx
580            .powi::<2>(&Repr::<2>::neg_zero(), 3.into())
581            .unwrap()
582            .value();
583        assert!(r.repr().is_neg_zero());
584        let _ = DBig::ZERO;
585    }
586}