Skip to main content

dashu_float/
root.rs

1use dashu_base::{
2    ring::DivRem, Approximation, CubicRoot, EstimatedLog2, Sign, SquareRoot, SquareRootRem,
3    UnsignedAbs,
4};
5use dashu_int::{IBig, UBig};
6
7use crate::{
8    ball::Ball,
9    error::{assert_limited_precision, panic_root_zeroth, FpError, FpResult},
10    fbig::FBig,
11    repr::{Context, Repr, Word},
12    round::{mode, ErrorBounds, Round, Rounding},
13    utils::{digit_len, shl_digits, split_digits_ref},
14};
15use core::cmp::Ordering;
16
17impl<R: ErrorBounds, const B: Word> SquareRoot for FBig<R, B> {
18    type Output = Self;
19    #[inline]
20    fn sqrt(&self) -> Self {
21        self.context.unwrap_fp(self.context.sqrt(self.repr()))
22    }
23}
24
25impl<R: Round, const B: Word> CubicRoot for FBig<R, B> {
26    type Output = Self;
27    #[inline]
28    fn cbrt(&self) -> Self {
29        self.context.unwrap_fp(self.context.cbrt(self.repr()))
30    }
31}
32
33impl<R: Round, const B: Word> FBig<R, B> {
34    /// Calculate the nth root of the floating point number.
35    ///
36    /// When `n` is large the computation can be expensive — the significand is
37    /// aligned to up to `n · precision` digits before the integer root is taken, and
38    /// the integer Newton iteration works with numbers of that size. For large
39    /// `n` consider [`powf`][`FBig::powf`] with a rational exponent `1 / n`
40    /// as a faster approximate alternative.
41    ///
42    /// # Examples
43    ///
44    /// ```
45    /// # use core::str::FromStr;
46    /// # use dashu_base::ParseError;
47    /// # use dashu_float::DBig;
48    /// let a = DBig::from_str("16")?;
49    /// assert_eq!(a.nth_root(4), DBig::from_str("2")?);
50    /// # Ok::<(), ParseError>(())
51    /// ```
52    ///
53    /// # Panics
54    ///
55    /// Panics if `n` is zero, or if `n` is even and the number is negative.
56    #[inline]
57    pub fn nth_root(&self, n: usize) -> Self {
58        self.context
59            .unwrap_fp(self.context.nth_root(n, self.repr()))
60    }
61}
62
63impl<R: Round> Context<R> {
64    /// Calculate the cubic root of the floating point number.
65    ///
66    /// # Examples
67    ///
68    /// ```
69    /// # use core::str::FromStr;
70    /// # use dashu_base::ParseError;
71    /// # use dashu_float::DBig;
72    /// use dashu_base::Approximation::*;
73    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
74    ///
75    /// let context = Context::<HalfAway>::new(2);
76    /// let a = DBig::from_str("8")?;
77    /// assert_eq!(context.cbrt(&a.repr()), Ok(Exact(DBig::from_str("2")?)));
78    /// # Ok::<(), ParseError>(())
79    /// ```
80    ///
81    /// # Panics
82    ///
83    /// Panics if the precision is unlimited.
84    #[inline]
85    pub fn cbrt<const B: Word>(&self, x: &Repr<B>) -> FpResult<FBig<R, B>> {
86        self.nth_root(3, x)
87    }
88
89    /// Calculate the nth root of the floating point number.
90    ///
91    /// # Examples
92    ///
93    /// ```
94    /// # use core::str::FromStr;
95    /// # use dashu_base::ParseError;
96    /// # use dashu_float::DBig;
97    /// use dashu_base::Approximation::*;
98    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
99    ///
100    /// let context = Context::<HalfAway>::new(2);
101    /// let a = DBig::from_str("27")?;
102    /// assert_eq!(context.nth_root(3, &a.repr()), Ok(Exact(DBig::from_str("3")?)));
103    /// # Ok::<(), ParseError>(())
104    /// ```
105    ///
106    /// # Panics
107    ///
108    /// Panics if `n` is zero, if the precision is unlimited, or if `n` is even and `x` is negative.
109    pub fn nth_root<const B: Word>(&self, n: usize, x: &Repr<B>) -> FpResult<FBig<R, B>> {
110        if x.is_infinite() {
111            return Err(FpError::InfiniteInput);
112        }
113        assert_limited_precision(self.precision);
114        if n == 0 {
115            panic_root_zeroth()
116        }
117        debug_assert!(n < isize::MAX as usize);
118        let sign = x.sign();
119        if sign == Sign::Negative && n % 2 == 0 {
120            return Err(FpError::OutOfDomain);
121        }
122        if n == 1 {
123            return Ok(self.repr_round_ref(x).map(|v| FBig::new(v, *self)));
124        }
125        if x.significand.is_zero() {
126            // UBig::ZERO.nth_root(n) erroneously returns ONE, so short-circuit here.
127            // An even root of -0 already errored above, so reaching here the sign is
128            // preserved: odd root of ±0 is ±0.
129            return Ok(Approximation::Exact(FBig::new(x.clone(), *self)));
130        }
131
132        // operate on the magnitude so that shifting/splitting keep a clean sign;
133        // the original sign is re-applied to the result at the end.
134        let xmag: IBig = if sign == Sign::Negative {
135            -&x.significand
136        } else {
137            x.significand.clone()
138        };
139
140        // Adjust the significand so that the exponent is divisible by n and the root of the
141        // aligned significand carries exactly `precision` digits. The alignment allows two
142        // shifts that differ by n digits: `+r` (padding the significand up) or `r − n`
143        // (truncating it, with the dropped part kept as the sticky `low`). When r > 0 the
144        // padding choice yields n·precision + r aligned digits whose root carries
145        // `precision + 1` digits, which would then have to be rounded to the precision in a
146        // second step — and the two roundings disagree exactly at midpoints of the coarse
147        // grid (round-to-integer-then-re-round is a double rounding). The truncating choice
148        // keeps the root at exactly `precision` digits, so a single rounding decides.
149        let digits = x.digits() as isize;
150        let r = (x.exponent + digits).rem_euclid(n as isize);
151        let shift =
152            n as isize * self.precision as isize - digits + r - n as isize * (r > 0) as isize;
153        let (signif, low, low_digits) = if shift > 0 {
154            (shl_digits::<B>(&xmag, shift as usize), IBig::ZERO, 0)
155        } else {
156            let shift = (-shift) as usize;
157            let (hi, lo) = split_digits_ref::<B>(&xmag, shift);
158            (hi, lo, shift)
159        };
160
161        let mag: UBig = signif.unsigned_abs();
162        let root: UBig = mag.nth_root(n);
163        let rem: UBig = &mag - root.clone().pow(n);
164        let exp = (x.exponent - shift) / n as isize;
165
166        let result_sign = if sign == Sign::Negative {
167            Sign::Negative
168        } else {
169            Sign::Positive
170        };
171        let signed_root: IBig = result_sign * root.clone();
172        debug_assert!(digit_len::<B>(root.as_ibig()) <= self.precision);
173
174        let res = if rem.is_zero() && low.is_zero() {
175            Approximation::Exact(signed_root)
176        } else {
177            // The true value is (root + frac)·BASE^exp with frac ∈ (0, 1), where root =
178            // floor(mag^(1/n)) and the fraction continues into the truncated input part
179            // `low`. Comparing frac against 1/2:
180            //   2·(root + frac) vs 2·root + 1
181            //   ⟺ 2^n·full vs (2·root + 1)^n·BASE^low_digits
182            // where full = mag·BASE^low_digits + low is the full aligned significand.
183            let adjust = R::round_low_part(&signed_root, result_sign, || {
184                let base_pow = Repr::<B>::BASE.pow(low_digits);
185                let full = &mag * &base_pow + low.unsigned_abs();
186                let lhs = full << n;
187                let rhs = ((root.clone() << 1) + UBig::from_word(1)).pow(n) * base_pow;
188                lhs.cmp(&rhs)
189            });
190            Approximation::Inexact(signed_root.clone() + adjust, adjust)
191        };
192        Ok(res
193            .map(|signif| Repr::new(signif, exp))
194            .map(|v| FBig::new(v, *self)))
195    }
196}
197
198impl<R: ErrorBounds> Context<R> {
199    /// Calculate the square root of the floating point number (correctly rounded).
200    ///
201    /// The integer square root of the exponent-aligned significand (which carries ~2·p digits) is
202    /// computed exactly, and its rounding to `p` digits is decided in a single step from the round
203    /// digit and the `sqrtrem` remainder (the sticky bit) — the same principle as MPFR. When the
204    /// root has `p + 1` digits it is rounded directly to `p` digits, avoiding the double rounding
205    /// that a rem-vs-root + re-round path incurs. For a power-of-two base this fast path is already
206    /// correctly rounded; for other bases the result is additionally certified by a Ziv loop (the
207    /// base-`B` digit alignment of an integer square root is only clean when the base is a power of
208    /// two).
209    ///
210    /// # Panics
211    ///
212    /// Panics if the precision is unlimited.
213    pub fn sqrt<const B: Word>(&self, x: &Repr<B>) -> FpResult<FBig<R, B>> {
214        if x.is_infinite() {
215            return Err(FpError::InfiniteInput);
216        }
217        if x.significand.is_zero() {
218            // sqrt(+0) = +0, sqrt(-0) = -0 (preserve the sign of zero). Exact, so handle
219            // it before the limited-precision assertion: a precision-0 (unlimited) value
220            // such as the one from `try_from(0.0)` must still compute sqrt(0) exactly.
221            return Ok(Approximation::Exact(FBig::new(x.clone(), *self)));
222        }
223        assert_limited_precision(self.precision);
224        if x.sign() == Sign::Negative {
225            return Err(FpError::OutOfDomain);
226        }
227
228        // One-step correctly-rounded sqrt of the (finite, positive, limited) input at a working
229        // precision, used directly by the power-of-two fast path and by the Ziv loop otherwise.
230        let sqrt_rounded = |guard: usize| -> FpResult<FBig<R, B>> {
231            let gctx = Context::<R>::new(self.precision + guard);
232            let p = gctx.precision;
233
234            // Adjust the significand so the exponent is even, with ~2p significant digits.
235            let digits = x.digits() as isize;
236            let shift = p as isize * 2 - (digits & 1) + (x.exponent & 1) - digits;
237            let (signif, low, low_digits) = if shift > 0 {
238                (shl_digits::<B>(&x.significand, shift as usize), IBig::ZERO, 0)
239            } else {
240                let shift = (-shift) as usize;
241                let (hi, lo) = split_digits_ref::<B>(&x.significand, shift);
242                (hi, lo, shift)
243            };
244
245            let (root, rem) = signif.unsigned_abs().sqrt_rem();
246            let exp = (x.exponent - shift) / 2;
247            let exact = rem.is_zero() && low.is_zero();
248            // The root has `p` or `p+1` base-B digits, decided exactly in O(1) from the shifted
249            // significand's digit count: `signif` carries `2p − (digits&1) + (exp&1)` digits, so it
250            // has 2p+1 digits (root has p+1) exactly when the significand's digit count is even and
251            // the input exponent is odd.
252            let root_is_p1 = (digits & 1) == 0 && (x.exponent & 1) == 1;
253
254            // The result's exponent. A p+1-digit root is rounded to p digits by dropping the lowest
255            // base-B digit (`r = root / B`), which shifts the value by one base-B digit: `exp + 1`.
256            // The arithmetic works on the unsigned `root`; it is converted to `IBig` only where
257            // `round_low_part` (signed) and the result's `+ Rounding` need it.
258            let (sig, adjust, result_exp) = if !root_is_p1 {
259                // p-digit root: the remainder (and any truncated input) decide the rounding. An
260                // integer sqrt has no exact half-tie (`sqrt(n) = root + 1/2` would require
261                // `4·rem = 2·root + 1`, impossible), so the rem-vs-root comparison is the exact
262                // single rounding.
263                let adjust = if exact {
264                    Rounding::NoOp
265                } else {
266                    R::round_low_part(root.as_ibig(), Sign::Positive, || {
267                        rem.cmp(&root)
268                            .then_with(|| (low << 2).cmp(&Repr::<B>::BASE.pow(low_digits).into()))
269                    })
270                };
271                (IBig::from(root), adjust, exp)
272            } else {
273                // p+1-digit root: round to p digits in one step. `r` = top p digits, `d` = round
274                // digit (a Word, from the single `div_rem`).
275                let (r, d) = root.div_rem(B);
276                let adjust = if exact {
277                    // The exact p+1-digit root: a clean rounding of `2·d` vs `B`, ties per the mode
278                    // (`2·d = B` is the real half-tie; `d vs ⌊B/2⌋` would mis-round odd bases).
279                    R::round_low_part(r.as_ibig(), Sign::Positive, || (d * 2).cmp(&B))
280                } else {
281                    // The true value is strictly above `root` (sticky remainder / truncated input),
282                    // so a round digit at the real half (2·d = B) is strictly past the half — report
283                    // Greater so every mode (including the directed ones) rounds from the correct
284                    // side. Delegating to `round_low_part` is what makes directed modes work: they
285                    // ignore the comparison and truncate/extend per their own direction.
286                    R::round_low_part(r.as_ibig(), Sign::Positive, || match (d * 2).cmp(&B) {
287                        Ordering::Equal => Ordering::Greater,
288                        other => other,
289                    })
290                };
291                (IBig::from(r), adjust, exp + 1)
292            };
293
294            let res = if exact && !root_is_p1 {
295                Approximation::Exact(sig)
296            } else {
297                Approximation::Inexact(sig + adjust, adjust)
298            };
299            Ok(res
300                .map(|signif| Repr::new(signif, result_exp))
301                .and_then(|v| gctx.repr_round(v))
302                .map(|v| FBig::new(v, gctx)))
303        };
304
305        if B.is_power_of_two() {
306            sqrt_rounded(0)
307        } else {
308            // Near-correct kernel, mechanical radius: the base-`B` digit alignment of the integer
309            // square root is only clean when the base is a power of two, so for other bases the
310            // single rounding step is bounded by one ulp at the working precision — the same
311            // assumption every other [`Ball`] operator makes of its kernel.
312            //
313            // Routing it through `Ball::from_rounded` also gives an *exact* root `rad == 0`, and
314            // that is load-bearing: a zero radius is the only one Ziv can certify against a
315            // one-sided directed preimage (`Down`'s `[y, y+ulp)` cannot contain `[y−r, y+r]` for
316            // any `r > 0`). With a blanket `value.ulp()` the loop never converged on a perfect
317            // square — `sqrt(4)` in base 10 under `Down`/`Up`/`Zero` doubled its working precision
318            // to ~10^8 digits instead of returning `2`.
319            let initial_guard = crate::utils::ceil_usize(self.precision.log2_est()) + 10;
320            self.ziv(initial_guard, |guard| {
321                let wp = self.precision + guard;
322                let rounded = sqrt_rounded(guard)?;
323                Ok(Ball::from_rounded(rounded.map(FBig::into_repr), wp)
324                    .to_value_radius::<R>(&Context::<R>::new(wp)))
325            })
326        }
327    }
328
329    /// Compute `sqrt(a² + b²)` without spurious overflow/underflow.
330    ///
331    /// This is the overflow-safe scaled sum-of-squares: the larger-magnitude operand is never
332    /// squared. Writing `m = max(|a|, |b|)` and `r = min(|a|,|b|) / m` (so `|r| ≤ 1`), the result is
333    /// `m · sqrt(1 + r²)`, where `1 + r² ∈ [1, 2]` cannot overflow. The result is correctly rounded
334    /// via a Ziv retry loop (`hypot(±inf, ·) = +inf`, `hypot(0, 0) = +0`).
335    ///
336    /// This is a field-arithmetic-class op (no constant cache), like `sqrt`/`atan2`.
337    ///
338    /// # Panics
339    ///
340    /// Panics if the precision is unlimited.
341    pub fn hypot<const B: Word>(&self, a: &Repr<B>, b: &Repr<B>) -> FpResult<FBig<R, B>> {
342        if a.is_infinite() || b.is_infinite() {
343            return Ok(Approximation::Exact(FBig::new(Repr::infinity(), *self)));
344        }
345        assert_limited_precision(self.precision);
346        if a.significand.is_zero() && b.significand.is_zero() {
347            return Ok(Approximation::Exact(FBig::new(Repr::zero(), *self)));
348        }
349
350        // magnitudes, ordered large >= small (both finite, not both zero here)
351        let a_mag = if a.sign() == Sign::Negative {
352            -a.clone()
353        } else {
354            a.clone()
355        };
356        let b_mag = if b.sign() == Sign::Negative {
357            -b.clone()
358        } else {
359            b.clone()
360        };
361        let (large, small) = if a_mag.cmp(&b_mag).is_ge() {
362            (a_mag, b_mag)
363        } else {
364            (b_mag, a_mag)
365        };
366
367        if small.significand.is_zero() {
368            // hypot(x, 0) = |x|; `large` is already a magnitude.
369            return Ok(self.repr_round_ref(&large).map(|v| FBig::new(v, *self)));
370        }
371
372        // The result is `sqrt(large² + small²)`, i.e. ∈ [large, large·√2]. It overflows only when
373        // `large` is so large that the result reaches the infinity sentinel exponent — unreachable
374        // for real inputs, but pre-checked here so the Ziv closure can use infallible `FBig`
375        // arithmetic.
376        if large.exponent >= isize::MAX - 1 {
377            return Err(FpError::Overflow(Sign::Positive));
378        }
379
380        let initial_guard = crate::utils::ceil_usize(self.precision.log2_est()) + 10;
381        self.ziv(initial_guard, |guard| {
382            let gctx = Context::<mode::HalfEven>::new(self.precision + guard);
383            // result = sqrt(large² + small²), with both operands scaled down by `k` base-B digits
384            // before squaring (so `large²` can't overflow the exponent) and the root scaled back:
385            // sqrt(L² + S²) · B^k = sqrt(large² + small²) for L = large·B⁻ᵏ, S = small·B⁻ᵏ. No
386            // division — so for integer inputs every step is exact (MPFR's `exact` flag), and an
387            // all-exact chain yields the exact true value. The tracking variants report radius 0
388            // then, which `ziv` accepts without the containment test (it can't certify an
389            // exactly-representable result under directed rounding — e.g. hypot(3,4)=5,
390            // hypot(5,12)=13 — which sits on a one-sided preimage boundary).
391            // Scale down by the largest raw exponent of the *two* operands: base-B
392            // normalization can leave the smaller value with the larger raw exponent
393            // (e.g. `4·2^e` normalizes to `1·2^(e+2)`), and it is small's square that
394            // would then collide with the infinity sentinel.
395            let k = (large.exponent.max(small.exponent) as i128 - (isize::MAX as i128 - 2) / 2)
396                .max(0) as isize;
397            let wp = gctx.precision;
398            // The input roundings' exactness folds through `from_rounded`: an exact input keeps
399            // `rad = 0`, so an all-exact chain (integer inputs, no rounding anywhere) carries a
400            // zero radius — the exactly-representable directed-rounding case that no nonzero
401            // radius could certify (e.g. hypot(3,4)=5, hypot(5,12)=13).
402            let large_ball = Ball::from_rounded(gctx.repr_round_ref(&large), wp);
403            let small_ball = Ball::from_rounded(gctx.repr_round_ref(&small), wp);
404            // The shifted balls are used twice (the square), so bind them once — a shift is a full
405            // O(p) clone otherwise.
406            let l = large_ball.shift(-k);
407            let s = small_ball.shift(-k);
408            let l_sq = l.mul(&l, wp)?;
409            let s_sq = s.mul(&s, wp)?;
410            let sum = l_sq.add(&s_sq, wp)?;
411            let root = sum.sqrt(wp)?;
412            let result = root.shift(k); // exact exponent shift — scales back, radius unchanged
413            Ok(result.to_value_radius::<R>(&Context::<R>::new(wp)))
414        })
415    }
416}
417
418impl<R: ErrorBounds, const B: Word> FBig<R, B> {
419    /// Calculate the square root of the floating point number (correctly rounded).
420    ///
421    /// # Panics
422    ///
423    /// Panics if the precision is unlimited.
424    #[inline]
425    pub fn sqrt(&self) -> Self {
426        self.context.unwrap_fp(self.context.sqrt(&self.repr))
427    }
428
429    /// Compute `sqrt(self² + other²)` without spurious overflow/underflow.
430    ///
431    /// The result precision is `max(self.precision(), other.precision())`. See
432    /// [`Context::hypot`] for the overflow-safety strategy.
433    ///
434    /// # Examples
435    ///
436    /// ```
437    /// # use core::str::FromStr;
438    /// # use dashu_base::ParseError;
439    /// # use dashu_float::DBig;
440    /// let a = DBig::from_str("3")?;
441    /// let b = DBig::from_str("4")?;
442    /// assert_eq!(a.hypot(&b), DBig::from_str("5")?);
443    /// # Ok::<(), ParseError>(())
444    /// ```
445    ///
446    /// # Panics
447    ///
448    /// Panics if the precision is unlimited.
449    #[inline]
450    pub fn hypot(&self, other: &Self) -> Self {
451        let context = Context::max(self.context, other.context);
452        context.unwrap_fp(context.hypot(&self.repr, &other.repr))
453    }
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459    use crate::round::mode;
460    use dashu_base::Approximation::*;
461
462    fn r2(sig: i32, exp: isize) -> Repr<2> {
463        Repr::new(sig.into(), exp)
464    }
465
466    // sqrt under directed modes must bracket the true value (issue #99): for a positive
467    // non-square x, sqrt_down(x)² < x < sqrt_up(x)² and the two results differ. The inputs
468    // 6 = 3·2¹ and 24 = 3·2³ exercise the p+1-digit integer-root path (even digit count, odd
469    // exponent) whose sticky remainder the old code rounded the wrong way under Up/Away. Every
470    // input here has a non-square odd part, so x is never a perfect square at any exponent.
471    #[test]
472    fn test_sqrt_directed_modes_bracket() {
473        for x in [3i32, 5, 6, 7, 10, 24, 30] {
474            for exp in [-3isize, -1, 0, 1, 3] {
475                let input = Repr::<2>::new(IBig::from(x), exp);
476                for p in [20usize, 50, 100, 500] {
477                    let down = Context::<mode::Down>::new(p)
478                        .sqrt(&input)
479                        .unwrap()
480                        .value()
481                        .with_precision(0)
482                        .value();
483                    let up = Context::<mode::Up>::new(p)
484                        .sqrt(&input)
485                        .unwrap()
486                        .value()
487                        .with_precision(0)
488                        .value();
489                    let x = FBig::<mode::HalfEven>::new(input.clone(), Context::new(0));
490                    // squares at unlimited precision are exact, so these comparisons are exact
491                    assert!(&down * &down < x, "sqrt({input:?}) @ p{p}: Down bound too high");
492                    assert!(&up * &up > x, "sqrt({input:?}) @ p{p}: Up bound too low");
493                    assert!(down < up, "sqrt({input:?}) @ p{p}: Down == Up on a non-square");
494                }
495            }
496        }
497    }
498
499    // Perfect squares are exact under every mode and yield identical Down/Up results.
500    #[test]
501    fn test_sqrt_perfect_square_is_exact() {
502        for (input, root) in [
503            (r2(1, 2), r2(1, 1)),
504            (r2(9, -2), r2(3, -1)),
505            (r2(25, -4), r2(5, -2)),
506        ] {
507            for p in [20usize, 50] {
508                let r = Context::<mode::HalfEven>::new(p).sqrt(&input).unwrap();
509                assert!(matches!(r, Exact(..)), "sqrt({input:?}) @ p{p} should be exact");
510                assert_eq!(r.value().repr(), &root);
511                assert_eq!(
512                    Context::<mode::Down>::new(p)
513                        .sqrt(&input)
514                        .unwrap()
515                        .value()
516                        .repr(),
517                    &root
518                );
519                assert_eq!(
520                    Context::<mode::Up>::new(p)
521                        .sqrt(&input)
522                        .unwrap()
523                        .value()
524                        .repr(),
525                    &root
526                );
527            }
528        }
529    }
530
531    // The same class of bug at tiny precision: sqrt(1.75) = 1.3228… has the p+1-digit integer
532    // root 5 (= 5·2^-2 = 1.25), exactly the midpoint of the p2 neighbours 1 and 1.5 — but the
533    // inexact root's remainder puts the true value strictly above the midpoint, so HalfEven
534    // must round up to 1.5 (the old code tied down to 1.0).
535    #[test]
536    fn test_sqrt_p1_digit_root_midpoint() {
537        let v = Context::<mode::HalfEven>::new(2)
538            .sqrt(&r2(7, -2))
539            .unwrap()
540            .value();
541        assert_eq!(v.repr(), &r2(3, -1));
542        // the same input under Up/Down brackets 1.3228… correctly
543        let up = Context::<mode::Up>::new(2)
544            .sqrt(&r2(7, -2))
545            .unwrap()
546            .value();
547        let down = Context::<mode::Down>::new(2)
548            .sqrt(&r2(7, -2))
549            .unwrap()
550            .value();
551        assert_eq!(up.repr(), &r2(3, -1));
552        assert_eq!(down.repr(), &r2(1, 0));
553    }
554
555    // An exact p+1-digit root is rounded to the precision in one step: sqrt(1.5625) = 1.25 is
556    // exactly the midpoint of 1.0 and 1.5; ties to even picks the significand 10₂ -> 1.0. The
557    // result differs from the exact root, so the operation is still Inexact.
558    #[test]
559    fn test_sqrt_exact_p1_digit_root_ties_to_even() {
560        let r = Context::<mode::HalfEven>::new(2).sqrt(&r2(25, -4)).unwrap();
561        assert!(matches!(r, Inexact(..)));
562        assert_eq!(r.value().repr(), &r2(1, 0));
563    }
564
565    // nth_root must round a precision+1-digit integer root in a single step (issue #100):
566    // the old code rounded the root to an integer first and re-rounded, which ties the wrong
567    // way exactly at midpoints of the coarse grid.
568    #[test]
569    fn test_nth_root_single_step_rounding() {
570        // nth_root(2, 1.75) @ p2 HalfEven = 1.5 (see test_sqrt_p1_digit_root_midpoint)
571        let v = Context::<mode::HalfEven>::new(2)
572            .nth_root(2, &r2(7, -2))
573            .unwrap()
574            .value();
575        assert_eq!(v.repr(), &r2(3, -1));
576
577        // exact root one digit wider than the precision: nth_root(2, 2.25) = 1.5 @ p2
578        let r = Context::<mode::HalfEven>::new(2)
579            .nth_root(2, &r2(9, -2))
580            .unwrap();
581        assert!(matches!(r, Exact(..)));
582        assert_eq!(r.value().repr(), &r2(3, -1));
583
584        // directed modes bracket on the same input
585        let up = Context::<mode::Up>::new(2)
586            .nth_root(2, &r2(7, -2))
587            .unwrap()
588            .value();
589        let down = Context::<mode::Down>::new(2)
590            .nth_root(2, &r2(7, -2))
591            .unwrap()
592            .value();
593        assert_eq!(up.repr(), &r2(3, -1));
594        assert_eq!(down.repr(), &r2(1, 0));
595
596        // agreement with sqrt on a perfect square: nth_root(2, 16) @ p5 = 4
597        let v = Context::<mode::HalfEven>::new(5)
598            .nth_root(2, &r2(1, 4))
599            .unwrap()
600            .value();
601        assert_eq!(v.repr(), &r2(1, 2));
602    }
603
604    #[test]
605    #[should_panic]
606    fn test_fbig_sqrt_negative_panics() {
607        // sqrt(-1) is out of domain; the FBig layer panics.
608        let neg_one = FBig::<mode::HalfEven>::try_from(-1.0f64).unwrap();
609        let _ = neg_one.sqrt();
610    }
611
612    #[test]
613    fn test_hypot_pythagorean() {
614        let ctx = Context::<mode::HalfEven>::new(53);
615        let mk = |v: i32| Repr::<2>::new(v.into(), 0);
616        // hypot(3, 4) = 5
617        let r = ctx.hypot(&mk(3), &mk(4)).unwrap().value();
618        assert_eq!(r.repr().significand(), &5.into());
619        // hypot(5, 0) = 5
620        let r = ctx.hypot(&mk(5), &mk(0)).unwrap().value();
621        assert_eq!(r.repr().significand(), &5.into());
622        // hypot(0, 0) = 0
623        let r = ctx.hypot(&mk(0), &mk(0)).unwrap().value();
624        assert!(r.repr().is_pos_zero());
625        // hypot(inf, x) = +inf
626        let r = ctx.hypot(&Repr::infinity(), &mk(3)).unwrap().value();
627        assert!(r.repr().is_infinite());
628        assert_eq!(r.repr().sign(), Sign::Positive);
629    }
630
631    fn check_hypot_exact_triples<R: ErrorBounds>(ctx: Context<R>) {
632        let mk = |v: i32| Repr::<2>::new(v.into(), 0);
633        // Pythagorean triples: the result is exactly representable, so under a directed mode it
634        // sits on a one-sided preimage boundary. The closure must terminate (radius 0 from the
635        // all-exact `sqrt(large²+small²)` chain) rather than infinite-retry.
636        for (a, b, h) in [(3, 4, 5), (5, 12, 13), (8, 15, 17)] {
637            let r = ctx.hypot(&mk(a), &mk(b)).unwrap().value();
638            assert_eq!(r.repr().significand(), &h.into(), "hypot({a}, {b})");
639        }
640    }
641
642    #[test]
643    fn test_hypot_exact_under_directed_rounding() {
644        check_hypot_exact_triples(Context::<mode::Down>::new(53));
645        check_hypot_exact_triples(Context::<mode::Up>::new(53));
646        check_hypot_exact_triples(Context::<mode::Zero>::new(53));
647    }
648
649    #[test]
650    fn test_hypot_no_spurious_overflow() {
651        // a value whose square would collide with the +inf sentinel exponent, but whose
652        // hypot is itself representable: hypot(a, 0) = |a| must not overflow via a².
653        let ctx = Context::<mode::HalfEven>::new(53);
654        // exponent near isize::MAX/2 so that a² would overflow, but |a| is fine
655        let a = Repr::<2>::new(IBig::from(3), isize::MAX / 2);
656        let r = ctx.hypot(&a, &Repr::<2>::zero()).unwrap().value();
657        assert_eq!(r.repr().exponent(), isize::MAX / 2);
658    }
659
660    #[test]
661    fn test_hypot_extreme_exponents_rescale_both_operands() {
662        // Both operands past the scale-down threshold (`k > 0`): the `B⁻ᵏ` rescale → square →
663        // root → `B^k` scale-back chain must keep the value (the zero-operand test above never
664        // exercises the shifted `small` operand). `4·2^e` normalizes to `1·2^(e+2)`, so the
665        // *smaller* value carries the larger raw exponent — the regression: `k` was derived
666        // from `large`'s exponent only, and the non-exact `hypot(7·2^e, 4·2^e)` died on
667        // `Err(Overflow)` through small's square.
668        let ctx = Context::<mode::HalfEven>::new(53);
669        let e = isize::MAX / 2 + 10; // large enough that k > 0
670        let a = Repr::<2>::new(IBig::from(3), e);
671        let b = Repr::<2>::new(IBig::from(4), e);
672        let r = ctx.hypot(&a, &b).unwrap().value();
673        assert_eq!(r.repr().significand(), &5.into(), "hypot(3·B^e, 4·B^e) = 5·B^e");
674        assert_eq!(r.repr().exponent(), e);
675        // the non-exact value (√65 · 2^e): a power-of-two rescale of the moderate-exponent
676        // result — same significand, exponent shifted by e
677        let a7 = Repr::<2>::new(IBig::from(7), e);
678        let r = ctx.hypot(&a7, &b).unwrap().value();
679        let base = ctx
680            .hypot(&Repr::<2>::new(IBig::from(7), 0), &Repr::<2>::new(IBig::from(4), 0))
681            .unwrap()
682            .value();
683        let expect = Repr::<2>::new(base.repr().significand().clone(), base.repr().exponent() + e);
684        assert_eq!(r.repr(), &expect, "hypot(7·2^e, 4·2^e) = √65·2^e");
685    }
686
687    /// A perfect square in a *non-power-of-two* base must certify under every rounding mode,
688    /// the one-sided directed ones included.
689    ///
690    /// Regression: `sqrt`'s Ziv closure reported a blanket `value.ulp()` radius, never zero, and
691    /// no `r > 0` fits inside `Down`'s preimage `[y, y+ulp)` — so an exactly-representable root
692    /// could not be certified at all. `sqrt(4)` in base 10 under `Down`/`Up`/`Zero` doubled its
693    /// working precision until the retry budget ran out (~10^8 digits) instead of returning `2`.
694    /// The root is now wrapped as a [`Ball`], so an exact result carries `rad == 0`.
695    #[test]
696    fn test_sqrt_exact_root_certifies_under_directed_rounding() {
697        // (input, expected exact root) — perfect squares, and a non-square for contrast
698        let cases: [(i64, isize, i64, isize, bool); 4] = [
699            (4, 0, 2, 0, true),
700            (100, 0, 10, 0, true),
701            (9, 0, 3, 0, true),
702            (25, -2, 5, -1, true), // 0.25 → 0.5
703        ];
704        macro_rules! check {
705            ($m:ty, $name:expr) => {
706                for (sig, exp, root_sig, root_exp, exact) in cases {
707                    let x = Repr::<10>::new(IBig::from(sig), exp);
708                    let r = Context::<$m>::new(10).sqrt::<10>(&x).unwrap();
709                    let want = Repr::<10>::new(IBig::from(root_sig), root_exp);
710                    assert_eq!(
711                        matches!(r, Approximation::Exact(_)),
712                        exact,
713                        "{}: sqrt({sig}e{exp}) exactness",
714                        $name
715                    );
716                    assert_eq!(r.value().repr(), &want, "{}: sqrt({sig}e{exp})", $name);
717                }
718            };
719        }
720        check!(mode::Down, "Down");
721        check!(mode::Up, "Up");
722        check!(mode::Zero, "Zero");
723        check!(mode::HalfEven, "HalfEven");
724        check!(mode::HalfAway, "HalfAway");
725    }
726}