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