Skip to main content

decimal_scaled/types/
powers.rs

1//! Power and root methods for [`D38`].
2//!
3//! # Methods
4//!
5//! - [`D38::pow`] — unsigned integer power via square-and-multiply over
6//! the `Mul` operator. Panics on overflow in debug builds; wraps in
7//! release builds. Matches `i128::pow` semantics.
8//! - [`D38::powi`] — signed integer power. For negative `exp`, returns
9//! `D38::ONE / self.pow(exp.unsigned_abs())`.
10//! - [`D38::powf`] — floating-point power via the f64 bridge. Lossy.
11//! Requires the `std` feature.
12//! - [`D38::sqrt`] — square root via the f64 bridge. IEEE 754 mandates
13//! that `f64::sqrt` is correctly-rounded, so identical inputs produce
14//! identical output bit-patterns on every conformant platform.
15//! Requires the `std` feature.
16//! - [`D38::cbrt`] — cube root via the f64 bridge. Defined for negative
17//! inputs. Requires the `std` feature.
18//! - [`D38::mul_add`] — `self * a + b` in one call. No hardware FMA;
19//! mirrors the `f64::mul_add` call shape so generic numeric code can
20//! monomorphise to `D38`. Always available.
21//! - [`D38::hypot`] — `sqrt(self^2 + other^2)` without intermediate
22//! overflow, using the scale-trick algorithm. Requires the `std`
23//! feature via `sqrt`.
24//!
25//! # The `*_strict` dual API
26//!
27//! `sqrt` / `cbrt` / `powf` / `hypot` each have an integer-only
28//! `*_strict` form and an f64-bridge form (see `docs/strict-mode.md`).
29//! The `*_strict` forms are **correctly rounded** — within 0.5 ULP of
30//! the exact result under the active [`RoundingMode`]:
31//!
32//! - `sqrt_strict` / `cbrt_strict` form the exact 256-/384-bit
33//! radicand and take its exact integer root, then apply the rounding
34//! mode (no ties exist for integer-sqrt so the three half-modes
35//! coincide; `Floor`/`Ceiling` divert for the directed cases);
36//! - `powf_strict` runs `exp(y·ln(x))` entirely in the `algos::fixed_d38`
37//! guard-digit intermediate;
38//! - `hypot_strict` composes `sqrt_strict` via the scale-trick.
39//!
40//! Each strict method has a `*_strict_with(mode)` sibling that takes
41//! the rounding mode explicitly; the no-arg `*_strict` form
42//! delegates to it with the crate-default mode (see
43//! [`crate::RoundingMode`] for the `rounding-*` Cargo features).
44//! `powf` additionally ships `powf_approx(working_digits)` and
45//! `powf_approx_with(working_digits, mode)` — the four-variant matrix
46//! the transcendentals expose; `sqrt` / `cbrt` / `hypot` have no
47//! guard-width parameter (the exact-integer-root path is precision-
48//! independent), so only the `_strict` / `_strict_with` pair exists.
49//!
50//! `pow` / `powi` (integer exponents) are exact at any feature
51//! configuration. The plain `sqrt` / `cbrt` / `powf` / `hypot`
52//! dispatch to the `*_strict` form under the `strict` feature, and to
53//! the f64 bridge otherwise; the `*_strict` forms are always compiled
54//! unless `fast` is set, and are `no_std`-compatible.
55//!
56//! [`RoundingMode`]: crate::RoundingMode
57//!
58//! # Variant family for `pow`
59//!
60//! - [`D38::checked_pow`] — `Option<Self>`, `None` on overflow at any
61//! step.
62//! - [`D38::wrapping_pow`] — two's-complement wrap at every step.
63//! - [`D38::saturating_pow`] — clamps to `D38::MAX` or `D38::MIN`
64//! based on the sign of the would-be result.
65//! - [`D38::overflowing_pow`] — `(Self, bool)`; the bool is `true` if
66//! any step overflowed, with the value equal to the wrapping form.
67//!
68//! # Square-and-multiply algorithm
69//!
70//! Starting from `acc = ONE`, the algorithm walks the bits of `exp` from
71//! low to high. On each iteration:
72//!
73//! 1. If the current bit of `exp` is set, multiply `acc *= base`.
74//! 2. Square `base *= base`.
75//!
76//! This costs `O(log exp)` multiplications rather than `O(exp)`. The
77//! variant family follows the same structure but applies the
78//! corresponding overflow arithmetic at every multiplication step.
79//!
80//! # `i32::MIN` edge case for `powi`
81//!
82//! `i32::unsigned_abs` returns `2_147_483_648_u32` for `i32::MIN`,
83//! avoiding the signed-negation overflow that `(-i32::MIN) as u32` would
84//! cause. `D38::ONE.powi(i32::MIN)` therefore evaluates correctly as
85//! `D38::ONE / D38::ONE.pow(2_147_483_648_u32)`.
86
87use crate::types::widths::D38;
88use crate::algos::mg_divide::mul_div_pow10;
89
90impl<const SCALE: u32> D38<SCALE> {
91    /// Raises `self` to the power `exp`.
92    ///
93    /// Uses square-and-multiply: walks the bits of `exp` from low to
94    /// high, squaring the base each step and accumulating when the
95    /// corresponding bit is set. Costs `O(log exp)` multiplications.
96    /// Each multiplication routes through the `D38` `Mul` operator.
97    ///
98    /// `exp = 0` always returns `ONE`, even when `self` is `ZERO`
99    /// (matches `i128::pow` convention).
100    ///
101    /// # Precision
102    ///
103    /// Strict: all arithmetic is integer-only; result is bit-exact.
104    ///
105    /// # Panics
106    ///
107    /// In debug builds, panics on `i128` overflow at any multiplication
108    /// step. In release builds, wraps two's-complement. Matches
109    /// `i128::pow` and `D38::Mul` semantics.
110    ///
111    /// Use [`Self::checked_pow`], [`Self::wrapping_pow`],
112    /// [`Self::saturating_pow`], or [`Self::overflowing_pow`] for
113    /// explicit overflow control.
114    ///
115    /// # Examples
116    ///
117    /// ```ignore
118    /// use decimal_scaled::D38s12;
119    /// let two = D38s12::from_int(2);
120    /// assert_eq!(two.pow(10), D38s12::from_int(1024));
121    /// // exp = 0 returns ONE regardless of base.
122    /// assert_eq!(D38s12::ZERO.pow(0), D38s12::ONE);
123    /// ```
124    #[inline]
125    #[must_use]
126    pub fn pow(self, exp: u32) -> Self {
127        let mut acc = Self::ONE;
128        let mut base = self;
129        let mut e = exp;
130        while e > 0 {
131            if e & 1 == 1 {
132                acc *= base;
133            }
134            e >>= 1;
135            if e > 0 {
136                base *= base;
137            }
138        }
139        acc
140    }
141
142    /// Raises `self` to the signed integer power `exp`.
143    ///
144    /// For non-negative `exp`, equivalent to `self.pow(exp as u32)`.
145    /// For negative `exp`, returns `D38::ONE / self.pow(exp.unsigned_abs())`,
146    /// i.e. the reciprocal of the positive-exponent form.
147    ///
148    /// # Precision
149    ///
150    /// Strict: all arithmetic is integer-only; result is bit-exact.
151    ///
152    /// # Panics
153    ///
154    /// - Overflow of `i128` storage at any step in debug builds (matches
155    /// [`Self::pow`]).
156    /// - Division by zero when `self == ZERO` and `exp < 0`.
157    ///
158    /// # Examples
159    ///
160    /// ```ignore
161    /// use decimal_scaled::D38s12;
162    /// let two = D38s12::from_int(2);
163    /// assert_eq!(two.powi(-1), D38s12::ONE / two);
164    /// assert_eq!(two.powi(0), D38s12::ONE);
165    /// assert_eq!(two.powi(3), D38s12::from_int(8));
166    /// ```
167    #[inline]
168    #[must_use]
169    pub fn powi(self, exp: i32) -> Self {
170        if exp >= 0 {
171            self.pow(exp as u32)
172        } else {
173            // unsigned_abs handles i32::MIN without signed-negation overflow.
174            Self::ONE / self.pow(exp.unsigned_abs())
175        }
176    }
177
178    /// Raises `self` to the power `exp` (strict integer-only stub).
179    ///
180    /// Converts both operands to f64, calls `f64::powf`, then converts
181    /// the result back. For integer exponents, prefer [`Self::pow`] or
182    /// [`Self::powi`], which are bit-exact.
183    ///
184    /// NaN results map to `ZERO`; infinities clamp to `MAX` or `MIN`,
185    /// following the saturate-vs-error policy of [`Self::from_f64`].
186    ///
187    /// # Precision
188    ///
189    /// Strict: all arithmetic is integer-only; result is bit-exact.
190    ///
191    /// # Examples
192    ///
193    /// ```ignore
194    /// use decimal_scaled::D38s12;
195    /// let two = D38s12::from_int(2);
196    /// let three = D38s12::from_int(3);
197    /// // 2^3 = 8, within f64 precision.
198    /// assert!((two.powf(three).to_f64() - 8.0).abs() < 1e-9);
199    /// ```
200    /// Raises `self` to the power `exp`, computed integer-only as
201    /// `exp(exp · ln(self))` — the `ln`, the `· exp`, and the `exp` all
202    /// run in the shared wide guard-digit intermediate, so the result
203    /// is correctly rounded (within 0.5 ULP).
204    ///
205    /// Always available, regardless of the `strict` feature. When
206    /// `strict` is enabled, the plain [`Self::powf`] delegates here.
207    ///
208    /// A zero or negative base saturates to `ZERO` (a negative base
209    /// with an arbitrary fractional exponent is not real-valued),
210    /// matching the f64-bridge NaN-to-ZERO policy.
211    #[inline]
212    #[must_use]
213    pub fn powf_strict(self, exp: D38<SCALE>) -> Self {
214        self.powf_strict_with(exp, crate::support::rounding::DEFAULT_ROUNDING_MODE)
215    }
216
217    /// `self^exp` under the supplied rounding mode.
218    #[inline]
219    #[must_use]
220    pub fn powf_strict_with(self, exp: D38<SCALE>, mode: crate::support::rounding::RoundingMode) -> Self {
221        <Self as crate::policy::pow::PowPolicy>::powf_impl(self, exp, mode)
222    }
223
224    /// `self^exp` with caller-chosen guard digits.
225    #[inline]
226    #[must_use]
227    pub fn powf_approx(self, exp: D38<SCALE>, working_digits: u32) -> Self {
228        self.powf_approx_with(exp, working_digits, crate::support::rounding::DEFAULT_ROUNDING_MODE)
229    }
230
231    /// `self^exp` with caller-chosen guard digits AND rounding mode.
232    #[inline]
233    #[must_use]
234    pub fn powf_approx_with(self, exp: D38<SCALE>, working_digits: u32, mode: crate::support::rounding::RoundingMode) -> Self {
235        if working_digits == crate::types::log_exp::STRICT_GUARD {
236            return self.powf_strict_with(exp, mode);
237        }
238        <Self as crate::policy::pow::PowPolicy>::powf_with_impl(self, exp, working_digits, mode)
239    }
240
241    /// Raises `self` to the power `exp`.
242    ///
243    /// With the `strict` feature enabled this is the integer-only
244    /// [`Self::powf_strict`]; without it, the f64-bridge form.
245    #[cfg(all(feature = "strict", not(feature = "fast")))]
246    #[inline]
247    #[must_use]
248    pub fn powf(self, exp: D38<SCALE>) -> Self {
249        self.powf_strict(exp)
250    }
251
252    /// Returns the square root of `self` (strict integer-only stub).
253    ///
254    /// IEEE 754 mandates that `f64::sqrt` is correctly-rounded
255    /// (round-to-nearest, ties-to-even). Combined with the deterministic
256    /// `to_f64` / `from_f64` round-trip, this makes
257    /// `D38::sqrt` bit-deterministic: the same input produces the same
258    /// output bit-pattern on every IEEE-754-conformant platform.
259    ///
260    /// Negative inputs produce a NaN from `f64::sqrt`, which
261    /// [`Self::from_f64`] maps to `ZERO` per the saturate-vs-error
262    /// policy. No panic is raised for negative inputs.
263    ///
264    /// # Precision
265    ///
266    /// Strict: all arithmetic is integer-only; result is bit-exact.
267    ///
268    /// # Examples
269    ///
270    /// ```ignore
271    /// use decimal_scaled::D38s12;
272    /// assert_eq!(D38s12::ZERO.sqrt(), D38s12::ZERO);
273    /// // f64::sqrt(1.0) == 1.0 exactly, so the result is bit-exact.
274    /// assert_eq!(D38s12::ONE.sqrt(), D38s12::ONE);
275    /// ```
276    #[inline]
277    #[must_use]
278    pub fn sqrt_strict(self) -> Self {
279        self.sqrt_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
280    }
281
282    /// Square root under the supplied rounding mode.
283    ///
284    /// Negative inputs saturate to [`Self::ZERO`] regardless of mode,
285    /// matching the f64-bridge policy.
286    ///
287    /// Body delegates to `policy::sqrt::SqrtPolicy::sqrt_impl`,
288    /// which for D38 selects the `mg_divide_d38` width-override kernel.
289    #[inline]
290    #[must_use]
291    pub fn sqrt_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
292        <Self as crate::policy::sqrt::SqrtPolicy>::sqrt_impl(self, mode)
293    }
294
295    /// Returns the square root of `self`.
296    ///
297    /// With the `strict` feature enabled this is the integer-only,
298    /// correctly-rounded [`Self::sqrt_strict`]; without it, the
299    /// f64-bridge form.
300    #[cfg(all(feature = "strict", not(feature = "fast")))]
301    #[inline]
302    #[must_use]
303    pub fn sqrt(self) -> Self {
304        self.sqrt_strict()
305    }
306
307    /// Returns the cube root of `self`.
308    ///
309    /// With the `strict` feature enabled this is the integer-only
310    /// [`Self::cbrt_strict`]; without it, the f64-bridge form.
311    #[cfg(all(feature = "strict", not(feature = "fast")))]
312    #[inline]
313    #[must_use]
314    pub fn cbrt(self) -> Self {
315        self.cbrt_strict()
316    }
317
318    /// Cube root of `self`. Defined for all reals — the sign of the
319    /// input is preserved (`cbrt(-8) = -2`).
320    ///
321    /// # Algorithm
322    ///
323    /// For a `D38<SCALE>` with raw storage `r`, the raw storage of the
324    /// cube root is
325    ///
326    /// round( cbrt(r / 10^SCALE) · 10^SCALE )
327    /// = round( cbrt(r · 10^(2·SCALE)) ).
328    ///
329    /// `r · 10^(2·SCALE)` is formed exactly as a 384-bit value and its
330    /// integer cube root is computed exactly, so the result is the
331    /// exact cube root correctly rounded to the type's last place
332    /// (within 0.5 ULP — the IEEE-754 round-to-nearest result).
333    ///
334    /// # Precision
335    ///
336    /// Strict: integer-only; correctly rounded.
337    #[inline]
338    #[must_use]
339    pub fn cbrt_strict(self) -> Self {
340        self.cbrt_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
341    }
342
343    /// Cube root under the supplied rounding mode. The sign of the
344    /// input is preserved; `Floor` / `Ceiling` resolve direction
345    /// relative to the signed result.
346    ///
347    /// Body delegates to `policy::cbrt::CbrtPolicy::cbrt_impl`.
348    #[inline]
349    #[must_use]
350    pub fn cbrt_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
351        <Self as crate::policy::cbrt::CbrtPolicy>::cbrt_impl(self, mode)
352    }
353
354    /// Returns `sqrt(self^2 + other^2)` without intermediate overflow,
355    /// computed integer-only via the correctly-rounded
356    /// [`Self::sqrt_strict`]. Same scale-trick algorithm as the
357    /// f64-bridge [`Self::hypot`]; available in `no_std`.
358    ///
359    /// Always available, regardless of the `strict` feature.
360    #[inline]
361    #[must_use]
362    pub fn hypot_strict(self, other: Self) -> Self {
363        self.hypot_strict_with(other, crate::support::rounding::DEFAULT_ROUNDING_MODE)
364    }
365
366    /// Hypot under the supplied rounding mode. The mode applies to the
367    /// inner square root; the surrounding adds and multiplies are
368    /// exact-or-truncating per the operator path's own contract.
369    #[inline]
370    #[must_use]
371    pub fn hypot_strict_with(self, other: Self, mode: crate::support::rounding::RoundingMode) -> Self {
372        let a = self.abs();
373        let b = other.abs();
374        let (large, small) = if a >= b { (a, b) } else { (b, a) };
375        if large == Self::ZERO {
376            Self::ZERO
377        } else {
378            let ratio = small / large;
379            let one_plus_sq = Self::ONE + ratio * ratio;
380            large * one_plus_sq.sqrt_strict_with(mode)
381        }
382    }
383
384    /// Returns `sqrt(self^2 + other^2)` without intermediate overflow.
385    ///
386    /// With the `strict` feature enabled this is the integer-only
387    /// [`Self::hypot_strict`]; without it, the f64-bridge form.
388    #[cfg(all(feature = "strict", not(feature = "fast")))]
389    #[inline]
390    #[must_use]
391    pub fn hypot(self, other: Self) -> Self {
392        self.hypot_strict(other)
393    }
394
395    // Overflow-variant family for pow.
396
397    /// Returns `Some(self^exp)`, or `None` if any multiplication step
398    /// overflows `i128`.
399    ///
400    /// Walks the same square-and-multiply as [`Self::pow`] but uses
401    /// `mul_div_pow10` (which returns `Option<i128>`) at each step.
402    /// The first `None` short-circuits to a `None` return.
403    ///
404    /// # Precision
405    ///
406    /// Strict: all arithmetic is integer-only; result is bit-exact.
407    ///
408    /// # Examples
409    ///
410    /// ```ignore
411    /// use decimal_scaled::D38s12;
412    /// // MAX^2 overflows.
413    /// assert!(D38s12::MAX.checked_pow(2).is_none());
414    /// // Any power of ONE is ONE.
415    /// assert_eq!(D38s12::ONE.checked_pow(1_000_000), Some(D38s12::ONE));
416    /// ```
417    #[inline]
418    #[must_use]
419    pub fn checked_pow(self, exp: u32) -> Option<Self> {
420        let mut acc: i128 = Self::ONE.0;
421        let mut base: i128 = self.0;
422        let mut e = exp;
423        while e > 0 {
424            if e & 1 == 1 {
425                acc = mul_div_pow10::<SCALE>(acc, base)?;
426            }
427            e >>= 1;
428            if e > 0 {
429                base = mul_div_pow10::<SCALE>(base, base)?;
430            }
431        }
432        Some(Self(acc))
433    }
434
435    /// Returns `self^exp`, wrapping two's-complement on overflow at
436    /// every multiplication step.
437    ///
438    /// Follows the same square-and-multiply structure as [`Self::pow`].
439    /// When a step overflows `mul_div_pow10`, the fallback is
440    /// `wrapping_mul` followed by `wrapping_div` of the scale
441    /// multiplier. The exact wrap pattern is deterministic and
442    /// reproducible but is not otherwise specified.
443    ///
444    /// # Precision
445    ///
446    /// Strict: all arithmetic is integer-only; result is bit-exact.
447    ///
448    /// # Examples
449    ///
450    /// ```ignore
451    /// use decimal_scaled::D38s12;
452    /// // ONE^N never overflows and returns ONE.
453    /// assert_eq!(D38s12::ONE.wrapping_pow(1_000_000), D38s12::ONE);
454    /// // MAX^2 wraps to a deterministic but unspecified value.
455    /// let _ = D38s12::MAX.wrapping_pow(2);
456    /// ```
457    #[inline]
458    #[must_use]
459    pub fn wrapping_pow(self, exp: u32) -> Self {
460        let mut acc: i128 = Self::ONE.0;
461        let mut base: i128 = self.0;
462        let mut e = exp;
463        let mult = Self::multiplier();
464        while e > 0 {
465            if e & 1 == 1 {
466                acc = match mul_div_pow10::<SCALE>(acc, base) {
467                    Some(q) => q,
468                    None => acc.wrapping_mul(base).wrapping_div(mult),
469                };
470            }
471            e >>= 1;
472            if e > 0 {
473                base = match mul_div_pow10::<SCALE>(base, base) {
474                    Some(q) => q,
475                    None => base.wrapping_mul(base).wrapping_div(mult),
476                };
477            }
478        }
479        Self(acc)
480    }
481
482    /// Returns `self^exp`, clamping to `D38::MAX` or `D38::MIN` on
483    /// overflow at any step.
484    ///
485    /// On the first step that overflows, the result is clamped based on
486    /// the sign of the mathematical result: positive overflows clamp to
487    /// `MAX`, negative overflows clamp to `MIN`. The sign of the result
488    /// is determined by `self.signum()` and whether `exp` is odd.
489    ///
490    /// `exp = 0` always returns `ONE` before entering the loop.
491    ///
492    /// # Precision
493    ///
494    /// Strict: all arithmetic is integer-only; result is bit-exact.
495    ///
496    /// # Examples
497    ///
498    /// ```ignore
499    /// use decimal_scaled::D38s12;
500    /// assert_eq!(D38s12::MAX.saturating_pow(2), D38s12::MAX);
501    /// assert_eq!(D38s12::ONE.saturating_pow(1_000_000), D38s12::ONE);
502    /// ```
503    #[inline]
504    #[must_use]
505    pub fn saturating_pow(self, exp: u32) -> Self {
506        // exp == 0: result is ONE by convention.
507        if exp == 0 {
508            return Self::ONE;
509        }
510        let mut acc: i128 = Self::ONE.0;
511        let mut base: i128 = self.0;
512        let mut e = exp;
513        // The final result is negative iff the base is negative and exp is odd.
514        let result_negative_if_overflow = self.0 < 0 && (exp & 1) == 1;
515        while e > 0 {
516            if e & 1 == 1 {
517                match mul_div_pow10::<SCALE>(acc, base) {
518                    Some(q) => acc = q,
519                    None => {
520                        return if result_negative_if_overflow {
521                            Self::MIN
522                        } else {
523                            Self::MAX
524                        };
525                    }
526                }
527            }
528            e >>= 1;
529            if e > 0 {
530                match mul_div_pow10::<SCALE>(base, base) {
531                    Some(q) => base = q,
532                    None => {
533                        // base*base is non-negative (squared); clamp by the
534                        // sign of the would-be final result.
535                        return if result_negative_if_overflow {
536                            Self::MIN
537                        } else {
538                            Self::MAX
539                        };
540                    }
541                }
542            }
543        }
544        Self(acc)
545    }
546
547    /// Returns `(self^exp, overflowed)`.
548    ///
549    /// `overflowed` is `true` if any multiplication step overflowed
550    /// `i128`. The returned value is the wrapping form (matches
551    /// [`Self::wrapping_pow`]).
552    ///
553    /// # Precision
554    ///
555    /// Strict: all arithmetic is integer-only; result is bit-exact.
556    ///
557    /// # Examples
558    ///
559    /// ```ignore
560    /// use decimal_scaled::D38s12;
561    /// let (_value, overflowed) = D38s12::MAX.overflowing_pow(2);
562    /// assert!(overflowed);
563    /// let (value, overflowed) = D38s12::ONE.overflowing_pow(5);
564    /// assert!(!overflowed);
565    /// assert_eq!(value, D38s12::ONE);
566    /// ```
567    #[inline]
568    #[must_use]
569    pub fn overflowing_pow(self, exp: u32) -> (Self, bool) {
570        let mut acc: i128 = Self::ONE.0;
571        let mut base: i128 = self.0;
572        let mut e = exp;
573        let mut overflowed = false;
574        let mult = Self::multiplier();
575        while e > 0 {
576            if e & 1 == 1 {
577                acc = if let Some(q) = mul_div_pow10::<SCALE>(acc, base) { q } else {
578                    overflowed = true;
579                    acc.wrapping_mul(base).wrapping_div(mult)
580                };
581            }
582            e >>= 1;
583            if e > 0 {
584                base = if let Some(q) = mul_div_pow10::<SCALE>(base, base) { q } else {
585                    overflowed = true;
586                    base.wrapping_mul(base).wrapping_div(mult)
587                };
588            }
589        }
590        (Self(acc), overflowed)
591    }
592}
593
594
595#[cfg(test)]
596mod tests {
597    use crate::types::widths::D38s12;
598
599    // Tolerance for f64-bridge tests. Used only by std-feature-gated
600    // tests below; gated to suppress unused-item warnings on no_std builds.
601    #[cfg(feature = "std")]
602    const TWO_LSB: i128 = 2;
603
604    #[cfg(feature = "std")]
605    fn within_lsb(actual: D38s12, expected: D38s12, lsb: i128) -> bool {
606        let diff = (actual.to_bits() - expected.to_bits()).abs();
607        diff <= lsb
608    }
609
610    // pow (integer)
611
612    /// `pow(0)` returns ONE for a nonzero base.
613    #[test]
614    fn pow_zero_is_one_for_nonzero() {
615        let v = D38s12::from_int(7);
616        assert_eq!(v.pow(0), D38s12::ONE);
617    }
618
619    /// `pow(1)` returns self.
620    #[test]
621    fn pow_one_is_self() {
622        let v = D38s12::from_int(7);
623        assert_eq!(v.pow(1), v);
624    }
625
626    /// `pow(2)` equals `self * self` for an integer value.
627    #[test]
628    fn pow_two_matches_mul() {
629        let v = D38s12::from_int(13);
630        assert_eq!(v.pow(2), v * v);
631    }
632
633    /// `pow(2)` equals `self * self` for a fractional value.
634    #[test]
635    fn pow_two_matches_mul_fractional() {
636        // 1.5 in raw bits at SCALE = 12.
637        let v = D38s12::from_bits(1_500_000_000_000);
638        assert_eq!(v.pow(2), v * v);
639    }
640
641    /// `2^10 == 1024`.
642    #[test]
643    fn pow_two_to_the_ten() {
644        let two = D38s12::from_int(2);
645        assert_eq!(two.pow(10), D38s12::from_int(1024));
646    }
647
648    /// `pow(0, 0) == ONE` — matches `i128::pow(0, 0) == 1`.
649    #[test]
650    fn zero_pow_zero_is_one() {
651        assert_eq!(D38s12::ZERO.pow(0), D38s12::ONE);
652    }
653
654    /// `pow(0, n)` for `n > 0` is `ZERO`.
655    #[test]
656    fn zero_pow_positive_is_zero() {
657        assert_eq!(D38s12::ZERO.pow(1), D38s12::ZERO);
658        assert_eq!(D38s12::ZERO.pow(5), D38s12::ZERO);
659    }
660
661    /// `pow(n)` of `ONE` is always `ONE`.
662    #[test]
663    fn one_pow_n_is_one() {
664        assert_eq!(D38s12::ONE.pow(0), D38s12::ONE);
665        assert_eq!(D38s12::ONE.pow(1), D38s12::ONE);
666        assert_eq!(D38s12::ONE.pow(100), D38s12::ONE);
667    }
668
669    /// `(-1)^n` alternates sign.
670    #[test]
671    fn negative_one_pow_alternates() {
672        let neg_one = -D38s12::ONE;
673        assert_eq!(neg_one.pow(0), D38s12::ONE);
674        assert_eq!(neg_one.pow(1), neg_one);
675        assert_eq!(neg_one.pow(2), D38s12::ONE);
676        assert_eq!(neg_one.pow(3), neg_one);
677    }
678
679    // powi (signed integer)
680
681    /// `powi(0)` returns ONE.
682    #[test]
683    fn powi_zero_is_one() {
684        let v = D38s12::from_int(7);
685        assert_eq!(v.powi(0), D38s12::ONE);
686    }
687
688    /// `powi(1)` returns self.
689    #[test]
690    fn powi_one_is_self() {
691        let v = D38s12::from_int(7);
692        assert_eq!(v.powi(1), v);
693    }
694
695    /// `powi(-1)` returns `ONE / self`.
696    #[test]
697    fn powi_minus_one_is_reciprocal() {
698        let v = D38s12::from_int(7);
699        assert_eq!(v.powi(-1), D38s12::ONE / v);
700    }
701
702    /// `powi(-3)` equals `ONE / pow(3)`.
703    #[test]
704    fn powi_negative_matches_reciprocal_of_positive() {
705        let v = D38s12::from_int(2);
706        assert_eq!(v.powi(-3), D38s12::ONE / v.pow(3));
707    }
708
709    /// `powi` agrees with `pow` for non-negative exponents.
710    #[test]
711    fn powi_positive_matches_pow() {
712        let v = D38s12::from_int(3);
713        for e in 0_i32..6 {
714            assert_eq!(v.powi(e), v.pow(e as u32));
715        }
716    }
717
718    /// `i32::MIN` edge: `unsigned_abs` returns 2_147_483_648; for a base
719    /// of ONE the result is ONE.
720    #[test]
721    fn powi_i32_min_for_one_base() {
722        assert_eq!(D38s12::ONE.powi(i32::MIN), D38s12::ONE);
723    }
724
725    // powf — requires std feature
726
727    /// `powf(0.5)` approximates `sqrt` within 2 LSB.
728    #[cfg(feature = "std")]
729    #[test]
730    fn powf_half_matches_sqrt() {
731        let v = D38s12::from_int(4);
732        let half = D38s12::from_bits(500_000_000_000); // 0.5 at SCALE=12
733        let powf_result = v.powf(half);
734        let sqrt_result = v.sqrt();
735        assert!(
736            within_lsb(powf_result, sqrt_result, TWO_LSB),
737            "powf(0.5)={}, sqrt={}, diff={}",
738            powf_result.to_bits(),
739            sqrt_result.to_bits(),
740            (powf_result.to_bits() - sqrt_result.to_bits()).abs(),
741        );
742    }
743
744    /// `powf(2)` agrees with `pow(2)` within 2 LSB (f64 bridge).
745    #[cfg(all(feature = "std", any(not(feature = "strict"), feature = "fast")))]
746    #[test]
747    fn powf_two_matches_pow_two_within_lsb() {
748        let v = D38s12::from_int(7);
749        let two = D38s12::from_int(2);
750        assert!(within_lsb(v.powf(two), v.pow(2), TWO_LSB));
751    }
752
753    /// Strict `powf` is correctly rounded: `powf(7, 2)` agrees with the
754    /// exact `pow(7, 2)` to within 1 ULP — the whole `exp(y·ln(x))`
755    /// chain runs in the shared wide guard-digit intermediate.
756    #[cfg(all(feature = "strict", not(feature = "fast")))]
757    #[test]
758    fn powf_two_matches_pow_two_within_lsb() {
759        let v = D38s12::from_int(7);
760        let two = D38s12::from_int(2);
761        assert!(within_lsb(v.powf(two), v.pow(2), 1));
762        // A few more integer-exponent cross-checks against exact `pow`.
763        for base in [2_i64, 3, 5, 11] {
764            let b = D38s12::from_int(base);
765            assert!(
766                within_lsb(b.powf(D38s12::from_int(3)), b.pow(3), 1),
767                "powf({base}, 3)"
768            );
769        }
770    }
771
772    // sqrt — requires std feature
773
774    /// `sqrt(0) == 0`.
775    #[cfg(feature = "std")]
776    #[test]
777    fn sqrt_zero_is_zero() {
778        assert_eq!(D38s12::ZERO.sqrt(), D38s12::ZERO);
779    }
780
781    /// `sqrt(1) == 1` — bit-exact because `f64::sqrt(1.0) == 1.0`.
782    #[cfg(feature = "std")]
783    #[test]
784    fn sqrt_one_is_one_bit_exact() {
785        assert_eq!(D38s12::ONE.sqrt(), D38s12::ONE);
786    }
787
788    /// `sqrt(4) == 2` — bit-exact because `f64::sqrt(4.0) == 2.0`.
789    #[cfg(feature = "std")]
790    #[test]
791    fn sqrt_four_is_two() {
792        let four = D38s12::from_int(4);
793        let two = D38s12::from_int(2);
794        assert_eq!(four.sqrt(), two);
795    }
796
797    /// Strict `sqrt` is correctly rounded: for the raw result `q`, the
798    /// scaled radicand `N = r · 10^SCALE` must satisfy
799    /// `(q − 0.5)² ≤ N ≤ (q + 0.5)²`, i.e. `q` is the exact square root
800    /// rounded to nearest. Checked exactly in 256-bit integer space
801    /// across several scales and magnitudes.
802    #[test]
803    fn strict_sqrt_is_correctly_rounded() {
804        // (q - 0.5)^2 = q^2 - q + 0.25 → lower bound  N ≥ q^2 - q + 1 (ints, when q>0)
805        // (q + 0.5)^2 = q^2 + q + 0.25 → upper bound  N ≤ q^2 + q
806        // So a correctly-rounded q satisfies q^2 - q < N ≤ q^2 + q  (q>0),
807        // or N == 0 when q == 0.
808        fn check<const S: u32>(raw: i128) {
809            let x = crate::types::widths::D38::<S>::from_bits(raw);
810            let q = x.sqrt_strict().to_bits();
811            assert!(q >= 0, "sqrt result must be non-negative");
812            // N = raw · 10^S as 256-bit; q is small enough that q^2 fits 256-bit.
813            let mult = 10u128.pow(S);
814            let (n_hi, n_lo) = crate::algos::mg_divide::mul2(raw as u128, mult);
815            let (qsq_hi, qsq_lo) = crate::algos::mg_divide::mul2(q as u128, q as u128);
816            // lower: N > q^2 - q ⇔   N + q > q^2   (q ≥ 0)
817            // upper: N ≤ q^2 + q
818            let q_u = q as u128;
819            // q^2 + q (256-bit)
820            let (uphi, uplo) = {
821                let (lo, c) = qsq_lo.overflowing_add(q_u);
822                (qsq_hi + c as u128, lo)
823            };
824            // N ≤ q^2 + q ?
825            let n_le_upper = n_hi < uphi || (n_hi == uphi && n_lo <= uplo);
826            assert!(n_le_upper, "sqrt({raw} @ s{S}) = {q}: N exceeds (q+0.5)^2");
827            if q > 0 {
828                // N + q (256-bit)
829                let (nphi, nplo) = {
830                    let (lo, c) = n_lo.overflowing_add(q_u);
831                    (n_hi + c as u128, lo)
832                };
833                // N + q > q^2 ?
834                let above_lower =
835                    nphi > qsq_hi || (nphi == qsq_hi && nplo > qsq_lo);
836                assert!(above_lower, "sqrt({raw} @ s{S}) = {q}: N below (q-0.5)^2");
837            }
838        }
839        for &raw in &[
840            1_i128,
841            2,
842            3,
843            4,
844            5,
845            999_999_999_999,
846            1_000_000_000_000,
847            1_500_000_000_000,
848            123_456_789_012_345,
849            i128::MAX,
850            i128::MAX / 7,
851        ] {
852            check::<0>(raw);
853            check::<6>(raw);
854            check::<12>(raw);
855            check::<19>(raw);
856        }
857        // High-scale cases where the radicand approaches the 256-bit cap.
858        for &raw in &[1_i128, 2, 17, i128::MAX, i128::MAX / 3] {
859            check::<38>(raw);
860        }
861    }
862
863    /// Strict `cbrt` is correctly rounded: for the raw result `q`, the
864    /// scaled radicand `N = |r| · 10^(2·SCALE)` must satisfy
865    /// `(2q − 1)³ < 8·N ≤ (2q + 1)³`, i.e. `q` is the exact cube root
866    /// rounded to nearest. Checked exactly in 384-bit integer space.
867    #[test]
868    fn strict_cbrt_is_correctly_rounded() {
869        // q correctly rounded ⇔  q − 0.5 < cbrt(N) ≤ q + 0.5
870        // ⇔  (2q − 1)³ < 8N ≤ (2q + 1)³.
871        // 384-bit comparison via num-bigint-free manual limbs would be
872        // verbose, so this check leans on the i256 dev-dependency to
873        // hold the 384-bit cubes (i256 is already a dev-dependency).
874        use i256::U256;
875        fn check<const S: u32>(raw: i128) {
876            let x = crate::types::widths::D38::<S>::from_bits(raw);
877            let q = x.cbrt_strict().to_bits();
878            // Sign must match the input.
879            assert_eq!(q.signum(), raw.signum(), "cbrt sign mismatch");
880            let qa = q.unsigned_abs();
881            let ra = raw.unsigned_abs();
882            // N = |r| · 10^(2S). 2S ≤ 76, so 10^(2S) needs U256; the
883            // product needs more than 256 bits at high S, so cap the
884            // scales exercised here to keep the check in U256 range.
885            // (The 384-bit path itself is exercised across all scales by
886            // the round-trip tests; this exact check covers S ≤ 25.)
887            let m = U256::from(10u8).pow(2 * S);
888            let n = U256::from(ra) * m;
889            let eight_n = n << 3;
890            let two_q = U256::from(qa) * U256::from(2u8);
891            let upper = {
892                let t = two_q + U256::from(1u8);
893                t * t * t
894            };
895            assert!(eight_n <= upper, "cbrt({raw} @ s{S}) = {q}: 8N exceeds (2q+1)^3");
896            if qa > 0 {
897                let t = two_q - U256::from(1u8);
898                let lower = t * t * t;
899                assert!(eight_n > lower, "cbrt({raw} @ s{S}) = {q}: 8N at/below (2q-1)^3");
900            }
901        }
902        for &raw in &[
903            1_i128, 2, 7, 8, 9, 26, 27, 28,
904            999_999_999_999, 1_000_000_000_000, 123_456_789_012_345,
905            -8, -27, -1_000_000_000_000,
906        ] {
907            check::<0>(raw);
908            check::<6>(raw);
909            check::<12>(raw);
910        }
911        // Larger magnitudes at low scale (still within the U256 check).
912        for &raw in &[i128::MAX, i128::MIN + 1, i128::MAX / 11] {
913            check::<0>(raw);
914            check::<2>(raw);
915        }
916    }
917
918    /// `sqrt(self * self) ~= self.abs()` within 2 LSB.
919    #[cfg(feature = "std")]
920    #[test]
921    fn sqrt_of_square_recovers_abs() {
922        let v = D38s12::from_bits(1_234_567_890_123);
923        let squared = v * v;
924        let recovered = squared.sqrt();
925        let abs_v = v.abs();
926        assert!(
927            within_lsb(recovered, abs_v, TWO_LSB),
928            "sqrt({})={}, expected~={}, diff={}",
929            squared.to_bits(),
930            recovered.to_bits(),
931            abs_v.to_bits(),
932            (recovered.to_bits() - abs_v.to_bits()).abs(),
933        );
934    }
935
936    /// `sqrt(self * self) ~= self.abs()` within 2 LSB for negative self.
937    #[cfg(feature = "std")]
938    #[test]
939    fn sqrt_of_square_negative_recovers_abs() {
940        let v = -D38s12::from_bits(4_567_891_234_567);
941        let squared = v * v;
942        let recovered = squared.sqrt();
943        let abs_v = v.abs();
944        assert!(within_lsb(recovered, abs_v, TWO_LSB));
945    }
946
947    /// A negative input produces NaN in f64, which maps to ZERO.
948    #[cfg(feature = "std")]
949    #[test]
950    fn sqrt_negative_saturates_to_zero() {
951        let v = -D38s12::from_int(4);
952        assert_eq!(v.sqrt(), D38s12::ZERO);
953    }
954
955    // cbrt — requires std feature
956
957    /// `cbrt(0) == 0`.
958    #[cfg(feature = "std")]
959    #[test]
960    fn cbrt_zero_is_zero() {
961        assert_eq!(D38s12::ZERO.cbrt(), D38s12::ZERO);
962    }
963
964    /// `cbrt(1) == 1`.
965    #[cfg(feature = "std")]
966    #[test]
967    fn cbrt_one_is_one() {
968        assert_eq!(D38s12::ONE.cbrt(), D38s12::ONE);
969    }
970
971    /// `cbrt(8) ~= 2` within 2 LSB.
972    #[cfg(feature = "std")]
973    #[test]
974    fn cbrt_eight_is_two() {
975        let eight = D38s12::from_int(8);
976        let two = D38s12::from_int(2);
977        assert!(within_lsb(eight.cbrt(), two, TWO_LSB));
978    }
979
980    /// `cbrt(-8) ~= -2` within 2 LSB.
981    #[cfg(feature = "std")]
982    #[test]
983    fn cbrt_minus_eight_is_minus_two() {
984        let neg_eight = D38s12::from_int(-8);
985        let neg_two = D38s12::from_int(-2);
986        assert!(
987            within_lsb(neg_eight.cbrt(), neg_two, TWO_LSB),
988            "cbrt(-8) = {}, expected ~ {}",
989            neg_eight.cbrt().to_bits(),
990            neg_two.to_bits(),
991        );
992    }
993
994    // checked_pow / wrapping_pow / saturating_pow / overflowing_pow
995
996    /// `checked_pow(MAX, 2)` is `None` because MAX^2 overflows.
997    #[test]
998    fn checked_pow_max_squared_is_none() {
999        assert!(D38s12::MAX.checked_pow(2).is_none());
1000    }
1001
1002    /// `checked_pow(ONE, N)` is `Some(ONE)` for any N.
1003    #[test]
1004    fn checked_pow_one_is_some_one() {
1005        assert_eq!(D38s12::ONE.checked_pow(1_000_000), Some(D38s12::ONE));
1006        assert_eq!(D38s12::ONE.checked_pow(0), Some(D38s12::ONE));
1007    }
1008
1009    /// `checked_pow` agrees with `pow` for non-overflowing inputs.
1010    #[test]
1011    fn checked_pow_matches_pow_when_no_overflow() {
1012        let v = D38s12::from_int(3);
1013        assert_eq!(v.checked_pow(0), Some(v.pow(0)));
1014        assert_eq!(v.checked_pow(1), Some(v.pow(1)));
1015        assert_eq!(v.checked_pow(5), Some(v.pow(5)));
1016    }
1017
1018    /// `saturating_pow(MAX, 2)` clamps to `MAX`.
1019    #[test]
1020    fn saturating_pow_max_squared_is_max() {
1021        assert_eq!(D38s12::MAX.saturating_pow(2), D38s12::MAX);
1022    }
1023
1024    /// `saturating_pow(MIN, 3)` clamps to `MIN` (negative result, odd exp).
1025    #[test]
1026    fn saturating_pow_min_cubed_is_min() {
1027        assert_eq!(D38s12::MIN.saturating_pow(3), D38s12::MIN);
1028    }
1029
1030    /// `saturating_pow(MIN, 2)` clamps to `MAX` (positive result, even exp).
1031    #[test]
1032    fn saturating_pow_min_squared_is_max() {
1033        assert_eq!(D38s12::MIN.saturating_pow(2), D38s12::MAX);
1034    }
1035
1036    /// `saturating_pow(ONE, N)` is ONE for any N.
1037    #[test]
1038    fn saturating_pow_one_is_one() {
1039        assert_eq!(D38s12::ONE.saturating_pow(1_000_000), D38s12::ONE);
1040    }
1041
1042    /// `saturating_pow(v, 0)` is ONE for any base.
1043    #[test]
1044    fn saturating_pow_zero_exp_is_one() {
1045        assert_eq!(D38s12::MAX.saturating_pow(0), D38s12::ONE);
1046        assert_eq!(D38s12::MIN.saturating_pow(0), D38s12::ONE);
1047    }
1048
1049    /// `overflowing_pow(MAX, 2)` returns `(wrapping_value, true)`.
1050    #[test]
1051    fn overflowing_pow_max_squared_flags_overflow() {
1052        let (value, overflowed) = D38s12::MAX.overflowing_pow(2);
1053        assert!(overflowed);
1054        assert_eq!(value, D38s12::MAX.wrapping_pow(2));
1055    }
1056
1057    /// `overflowing_pow(ONE, N)` never overflows.
1058    #[test]
1059    fn overflowing_pow_one_no_overflow() {
1060        let (value, overflowed) = D38s12::ONE.overflowing_pow(1_000_000);
1061        assert!(!overflowed);
1062        assert_eq!(value, D38s12::ONE);
1063    }
1064
1065    /// `overflowing_pow(v, 0)` is `(ONE, false)`.
1066    #[test]
1067    fn overflowing_pow_zero_exp_no_overflow() {
1068        let (value, overflowed) = D38s12::MAX.overflowing_pow(0);
1069        assert!(!overflowed);
1070        assert_eq!(value, D38s12::ONE);
1071    }
1072
1073    /// `wrapping_pow(MAX, 2)` matches the value half of `overflowing_pow`.
1074    #[test]
1075    fn wrapping_pow_max_squared_matches_overflowing() {
1076        let wrap = D38s12::MAX.wrapping_pow(2);
1077        let (over, _flag) = D38s12::MAX.overflowing_pow(2);
1078        assert_eq!(wrap, over);
1079    }
1080
1081    /// `wrapping_pow(ONE, N)` is ONE.
1082    #[test]
1083    fn wrapping_pow_one_is_one() {
1084        assert_eq!(D38s12::ONE.wrapping_pow(1_000_000), D38s12::ONE);
1085    }
1086
1087    /// `wrapping_pow` agrees with `pow` for non-overflowing inputs.
1088    #[test]
1089    fn wrapping_pow_matches_pow_when_no_overflow() {
1090        let v = D38s12::from_int(3);
1091        for e in 0..6 {
1092            assert_eq!(v.wrapping_pow(e), v.pow(e));
1093        }
1094    }
1095
1096    /// `pow(2) == self * self` for several representative raw values.
1097    #[test]
1098    fn pow_two_property_safe_values() {
1099        for raw in [
1100            1_234_567_890_123_i128,
1101            4_567_891_234_567_i128,
1102            7_890_123_456_789_i128,
1103        ] {
1104            let v = D38s12::from_bits(raw);
1105            assert_eq!(v.pow(2), v * v, "raw bits {raw}");
1106        }
1107    }
1108
1109    // mul_add (always available)
1110
1111    /// `mul_add(0, 0, 0) == 0`.
1112    #[test]
1113    fn mul_add_zero_zero_zero_is_zero() {
1114        let z = D38s12::ZERO;
1115        assert_eq!(z.mul_add(z, z), D38s12::ZERO);
1116    }
1117
1118    /// `mul_add(2, 3, 4) == 10`.
1119    #[test]
1120    fn mul_add_two_three_four_is_ten() {
1121        let two = D38s12::from_int(2);
1122        let three = D38s12::from_int(3);
1123        let four = D38s12::from_int(4);
1124        assert_eq!(two.mul_add(three, four), D38s12::from_int(10));
1125    }
1126
1127    /// `mul_add(self, ONE, ZERO) == self`.
1128    #[test]
1129    fn mul_add_identity_collapses() {
1130        let v = D38s12::from_int(7);
1131        assert_eq!(v.mul_add(D38s12::ONE, D38s12::ZERO), v);
1132    }
1133
1134    /// `mul_add(self, ZERO, b) == b`.
1135    #[test]
1136    fn mul_add_zero_factor_yields_addend() {
1137        let v = D38s12::from_int(7);
1138        let b = D38s12::from_int(13);
1139        assert_eq!(v.mul_add(D38s12::ZERO, b), b);
1140    }
1141
1142    /// `mul_add(a, b, c) == a * b + c` for representative raw values.
1143    #[test]
1144    fn mul_add_matches_mul_then_add_safe_values() {
1145        for (a_raw, b_raw, c_raw) in [
1146            (1_234_567_890_123_i128, 2_345_678_901_234_i128, 3_456_789_012_345_i128),
1147            (4_567_891_234_567_i128, 5_678_912_345_678_i128, 6_789_123_456_789_i128),
1148            (7_890_123_456_789_i128, 8_901_234_567_891_i128, 9_012_345_678_912_i128),
1149        ] {
1150            let a = D38s12::from_bits(a_raw);
1151            let b = D38s12::from_bits(b_raw);
1152            let c = D38s12::from_bits(c_raw);
1153            assert_eq!(
1154                a.mul_add(b, c),
1155                a * b + c,
1156                "raw bits ({a_raw}, {b_raw}, {c_raw})",
1157            );
1158        }
1159    }
1160
1161    /// `(-a).mul_add(b, c)` propagates the sign through the multiply step.
1162    #[test]
1163    fn mul_add_sign_propagates_through_factor() {
1164        let a = D38s12::from_int(3);
1165        let b = D38s12::from_int(5);
1166        let c = D38s12::from_int(7);
1167        // (-3) * 5 + 7 = -15 + 7 = -8
1168        assert_eq!((-a).mul_add(b, c), D38s12::from_int(-8));
1169    }
1170
1171    // hypot — requires std feature
1172
1173    // Tolerance for hypot: composes divide + square + add + sqrt + multiply,
1174    // each with up to 1 LSB of f64-bridge slack; sqrt quantisation dominates.
1175    #[cfg(feature = "std")]
1176    const HYPOT_TOLERANCE_LSB: i128 = 32;
1177
1178    /// `hypot(3, 4) ~= 5` — the Pythagorean triple.
1179    #[cfg(feature = "std")]
1180    #[test]
1181    fn hypot_three_four_is_five() {
1182        let three = D38s12::from_int(3);
1183        let four = D38s12::from_int(4);
1184        let five = D38s12::from_int(5);
1185        let result = three.hypot(four);
1186        assert!(
1187            within_lsb(result, five, HYPOT_TOLERANCE_LSB),
1188            "hypot(3, 4)={}, expected~={}, diff={}",
1189            result.to_bits(),
1190            five.to_bits(),
1191            (result.to_bits() - five.to_bits()).abs(),
1192        );
1193    }
1194
1195    /// `hypot(0, 0) == 0` — bit-exact via the early-return path.
1196    #[cfg(feature = "std")]
1197    #[test]
1198    fn hypot_zero_zero_is_zero_bit_exact() {
1199        assert_eq!(D38s12::ZERO.hypot(D38s12::ZERO), D38s12::ZERO);
1200    }
1201
1202    /// `hypot(0, x) ~= x.abs()` for nonzero x.
1203    #[cfg(feature = "std")]
1204    #[test]
1205    fn hypot_zero_x_is_abs_x() {
1206        let x = D38s12::from_int(7);
1207        let result = D38s12::ZERO.hypot(x);
1208        assert!(
1209            within_lsb(result, x.abs(), HYPOT_TOLERANCE_LSB),
1210            "hypot(0, 7)={}, expected~={}",
1211            result.to_bits(),
1212            x.abs().to_bits(),
1213        );
1214    }
1215
1216    /// `hypot(x, 0) ~= x.abs()` for nonzero x.
1217    #[cfg(feature = "std")]
1218    #[test]
1219    fn hypot_x_zero_is_abs_x() {
1220        let x = D38s12::from_int(-9);
1221        let result = x.hypot(D38s12::ZERO);
1222        assert!(
1223            within_lsb(result, x.abs(), HYPOT_TOLERANCE_LSB),
1224            "hypot(-9, 0)={}, expected~={}",
1225            result.to_bits(),
1226            x.abs().to_bits(),
1227        );
1228    }
1229
1230    /// `hypot(-a, b) == hypot(a, b)` — sign invariance from the abs step.
1231    #[cfg(feature = "std")]
1232    #[test]
1233    fn hypot_sign_invariant() {
1234        let three = D38s12::from_int(3);
1235        let four = D38s12::from_int(4);
1236        let pos = three.hypot(four);
1237        let neg_a = (-three).hypot(four);
1238        let neg_b = three.hypot(-four);
1239        let neg_both = (-three).hypot(-four);
1240        assert_eq!(pos, neg_a);
1241        assert_eq!(pos, neg_b);
1242        assert_eq!(pos, neg_both);
1243    }
1244
1245    /// `hypot` does not panic at large magnitudes that the naive form
1246    /// would overflow.
1247    ///
1248    /// At SCALE=12 with `i128::MAX / 2` raw bits, the true hypotenuse
1249    /// is well below `D38::MAX / sqrt(2)`, so no overflow occurs and
1250    /// the result is a nonzero positive value.
1251    #[cfg(feature = "std")]
1252    #[test]
1253    fn hypot_large_magnitudes_does_not_panic() {
1254        let half_max = D38s12::from_bits(i128::MAX / 2);
1255        let result = half_max.hypot(half_max);
1256        assert!(result > D38s12::ZERO);
1257        assert!(result >= half_max);
1258    }
1259
1260    /// `hypot(a, b)` matches the naive `sqrt(a^2 + b^2)` within tolerance
1261    /// for small magnitudes where the naive form does not overflow.
1262    #[cfg(feature = "std")]
1263    #[test]
1264    fn hypot_matches_naive_sqrt_of_sum_of_squares() {
1265        let a = D38s12::from_int(12);
1266        let b = D38s12::from_int(13);
1267        let h = a.hypot(b);
1268        let naive = (a * a + b * b).sqrt();
1269        assert!(
1270            within_lsb(h, naive, HYPOT_TOLERANCE_LSB),
1271            "hypot(12, 13)={}, naive sqrt(a^2+b^2)={}, diff={}",
1272            h.to_bits(),
1273            naive.to_bits(),
1274            (h.to_bits() - naive.to_bits()).abs(),
1275        );
1276    }
1277}
1278