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