Skip to main content

la_stack/
exact.rs

1#![forbid(unsafe_code)]
2
3//! Exact arithmetic operations via arbitrary-precision rational numbers.
4//!
5//! This module is only compiled when the `"exact"` Cargo feature is enabled.
6//! Exactness begins with the finite binary64 values already stored in
7//! [`Matrix`] and [`Vector`]: each value is lifted losslessly to a rational.
8//! These APIs cannot recover information rounded away before construction.
9//!
10//! # Architecture
11//!
12//! ## Determinants
13//!
14//! All determinant methods (`det_exact`, `det_exact_f64`,
15//! `det_exact_rounded_f64`, and `det_sign_exact`) share the same integer-scaled
16//! determinant core. Each proven-finite f64 entry is decomposed via
17//! `decompose_proven_finite_f64` into `mantissa × 2^exponent`, then all entries
18//! are scaled to a common `BigInt`
19//! matrix (shifting by `e - e_min`). D≤4 uses direct integer expansions; larger
20//! matrices use fraction-free Bareiss elimination \[7\] entirely in `BigInt`
21//! arithmetic — no `BigRational`, no GCD, no denominator tracking. The result
22//! is `(det_int, total_exp)` where `det = det_int × 2^(D × e_min)`. `det_exact`
23//! wraps this with `big_int_exp_to_big_rational` to reconstruct a reduced
24//! `BigRational`; `det_exact_f64` converts the same pair only when the exact
25//! value is representable as finite binary64; `det_exact_rounded_f64` rounds
26//! the same exact value to finite binary64; and `det_sign_exact` reads the sign
27//! directly from `det_int` (the scale factor is always positive).
28//!
29//! `det_sign_exact` adds a two-stage adaptive-precision optimisation inspired
30//! by Shewchuk's robust geometric predicates \[8\]:
31//!
32//! 1. **Fast filter (D ≤ 4)**: compute `det_direct()` and a conservative error
33//!    bound. If `|det| > bound`, the f64 sign is provably correct — return
34//!    immediately without allocating.
35//! 2. **Exact fallback**: evaluate the scaled `BigInt` matrix directly for
36//!    D ≤ 4 or with Bareiss elimination for D ≥ 5, yielding a
37//!    guaranteed-correct sign.
38//!
39//! ## Linear system solve
40//!
41//! `solve_exact`, `solve_exact_f64`, and `solve_exact_rounded_f64` solve
42//! `A x = b` with a hybrid algorithm that shares the determinant path's exact
43//! integer scaling and then applies Bareiss elimination to the augmented
44//! system. Matrix and RHS entries are decomposed via
45//! `decompose_proven_finite_f64` into `mantissa × 2^exponent`. Each side first
46//! derives an independent scale from its minimum exponent. When those scales
47//! differ by at most `MAX_SHARED_SCALE_GAP_BITS` (64), both sides use the lower
48//! scale; larger gaps retain the independent scales. The resulting solution is
49//! adjusted by the exact power-of-two ratio between the selected scales. This
50//! shares common factors when inexpensive without inflating one side's integers
51//! across a large exponent gap.
52//! Forward elimination runs entirely in `BigInt` with
53//! fraction-free Bareiss updates \[7\] — no `BigRational`, no GCD
54//! normalisation in the `O(D³)` phase.  Once the system is upper
55//! triangular, back-substitution is performed in `BigRational`, where
56//! fractions are inherent; this phase is only `O(D²)` so the rational
57//! overhead is modest.  First-non-zero pivoting is used throughout;
58//! since all arithmetic is exact, any non-zero pivot gives the correct
59//! result (no numerical stability concern). Every finite `f64` is exactly
60//! representable as a rational, so the result is exact for the stored inputs.
61//! `solve_exact_f64` returns `Vector<D>` only when every exact component is
62//! exactly representable as finite binary64; `solve_exact_rounded_f64` returns
63//! the exact components rounded to finite binary64.
64//!
65//! ## f64 → integer decomposition
66//!
67//! Both the determinant and solve paths share a single conversion
68//! primitive, `decompose_proven_finite_f64`, which parses the IEEE 754 binary64 bit
69//! representation into a proof-bearing component (\[9\]). The
70//! determinant path combines those components into a `BigInt` matrix for
71//! direct expansion or Bareiss elimination and a `2^(D × e_min)` scale factor,
72//! while the solve
73//! path builds a `BigInt` augmented system and lifts the
74//! upper-triangular result into `BigRational` for back-substitution.
75//! See Goldberg \[10\] for background on floating-point representation
76//! and conversion. Reference numbers refer to
77//! `REFERENCES.md`.
78//!
79//! ## Validation
80//!
81//! Public `Matrix` / `Vector` values are finite by construction before exact
82//! methods reach the integer-scaled exact core. The decomposition helpers consume
83//! that proof without repeating stored-entry validation; a fallible raw-f64
84//! decomposition remains only to test rejection at the primitive boundary.
85
86use core::hint::cold_path;
87use core::mem::take;
88use core::num::NonZeroU64;
89use std::array::from_fn;
90
91use num_bigint::{BigInt, Sign};
92use num_rational::BigRational;
93use num_traits::ToPrimitive;
94
95use crate::matrix::Matrix;
96use crate::vector::Vector;
97use crate::{LaError, UnrepresentableReason};
98
99/// The exact sign of a determinant.
100///
101/// Available with the `exact` Cargo feature.
102///
103/// This type makes the three possible outcomes explicit instead of exposing a
104/// raw integer that could contain values other than −1, 0, or +1.
105///
106/// # Examples
107/// ```
108/// use la_stack::prelude::*;
109///
110/// let sign = Matrix::<2>::identity().det_sign_exact();
111/// assert_eq!(sign, DeterminantSign::Positive);
112/// assert_eq!(sign.as_i8(), 1);
113/// ```
114#[must_use]
115#[derive(Clone, Copy, Debug, Eq, PartialEq)]
116pub enum DeterminantSign {
117    /// The determinant is strictly negative.
118    Negative,
119    /// The determinant is exactly zero.
120    Zero,
121    /// The determinant is strictly positive.
122    Positive,
123}
124
125impl DeterminantSign {
126    /// Return the conventional numeric sign −1, 0, or +1.
127    #[inline]
128    #[must_use]
129    pub const fn as_i8(self) -> i8 {
130        match self {
131            Self::Negative => -1,
132            Self::Zero => 0,
133            Self::Positive => 1,
134        }
135    }
136}
137
138/// Convert an already-computed exact result to finite binary64 output.
139///
140/// This extension trait is implemented for [`BigRational`] determinants and
141/// `[BigRational; D]` exact solutions. It lets callers retain the exact value,
142/// try the strict no-rounding contract, and recover with explicit rounding
143/// without repeating determinant evaluation or linear-system elimination.
144/// [`BigRational::new_raw`] values are interpreted by their mathematical
145/// quotient: denominator signs and common factors do not change the result. A
146/// zero denominator is rejected as [`UnrepresentableReason::NotFinite`].
147///
148/// # Examples
149/// ```
150/// use la_stack::prelude::*;
151///
152/// # fn main() -> Result<(), LaError> {
153/// let matrix = Matrix::<2>::try_from_rows([
154///     [1.0 + f64::EPSILON, 0.0],
155///     [0.0, 1.0 - f64::EPSILON],
156/// ])?;
157/// let exact = matrix.det_exact()?;
158/// let rounded = match exact.try_to_f64() {
159///     Ok(value) => value,
160///     Err(error) if error.requires_rounding() => exact.to_rounded_f64()?,
161///     Err(error) => return Err(error),
162/// };
163/// assert_eq!(rounded.to_bits(), 1.0_f64.to_bits());
164///
165/// let system = Matrix::<1>::try_from_rows([[3.0]])?;
166/// let rhs = Vector::<1>::try_new([3.0])?;
167/// let exact_solution = system.solve_exact(rhs)?;
168/// assert_eq!(exact_solution.try_to_f64()?.into_array(), [1.0]);
169/// # Ok(())
170/// # }
171/// ```
172pub trait ExactF64Conversion {
173    /// Finite binary64 output produced by the conversion.
174    type Output;
175
176    /// Convert only when every exact value already has an exact finite
177    /// binary64 representation.
178    ///
179    /// The candidate conversion follows IEEE 754 round-to-nearest,
180    /// ties-to-even, but this strict method returns it only when no rounding is
181    /// required.
182    ///
183    /// # Errors
184    /// Returns [`LaError::Unrepresentable`] with
185    /// [`UnrepresentableReason::RequiresRounding`] when finite binary64 output
186    /// would require rounding, or [`UnrepresentableReason::NotFinite`] when
187    /// rounding cannot produce finite output. Exact solution errors include the
188    /// first failing component index.
189    fn try_to_f64(&self) -> Result<Self::Output, LaError>;
190
191    /// Round the exact value to finite binary64 output.
192    ///
193    /// Rounding follows IEEE 754 round-to-nearest, ties-to-even.
194    ///
195    /// # Errors
196    /// Returns [`LaError::Unrepresentable`] with
197    /// [`UnrepresentableReason::NotFinite`] when rounding cannot produce finite
198    /// output. Exact solution errors include the first failing component index.
199    fn to_rounded_f64(&self) -> Result<Self::Output, LaError>;
200}
201
202const F64_SIGNIFICAND_BITS: i64 = 53;
203const F64_FRACTION_BITS: i64 = 52;
204const F64_MIN_BINARY_EXPONENT: i64 = -1074;
205const F64_MIN_NORMAL_EXPONENT: i64 = -1022;
206const F64_MAX_BINARY_EXPONENT: i64 = 1023;
207const F64_EXPONENT_BIAS: i64 = 1023;
208const F64_FRACTION_MASK: u64 = (1u64 << 52) - 1;
209
210/// Decompose an `f64` whose finiteness has already been proven into its IEEE
211/// 754 components.
212///
213/// This helper is total for every bit pattern so proof-bearing callers never
214/// need to recover from a second finiteness check. Its result is meaningful as
215/// an exact real value only when `x` is finite.
216const fn decompose_proven_finite_f64(x: f64) -> Component {
217    let bits = x.to_bits();
218    let biased_exp = ((bits >> 52) & 0x7FF) as i32;
219    let fraction = bits & 0x000F_FFFF_FFFF_FFFF;
220
221    // ±0.0
222    if biased_exp == 0 && fraction == 0 {
223        return Component::Zero;
224    }
225
226    let (mantissa, raw_exp) = if biased_exp == 0 {
227        // Subnormal: (-1)^s × 0.fraction × 2^(-1022)
228        //          = (-1)^s × fraction × 2^(-1074)
229        (fraction, -1074_i32)
230    } else {
231        // Normal: (-1)^s × 1.fraction × 2^(biased_exp - 1023)
232        //       = (-1)^s × (2^52 | fraction) × 2^(biased_exp - 1075)
233        ((1u64 << 52) | fraction, biased_exp - 1075)
234    };
235
236    // Strip trailing zeros so the mantissa is odd. The zero bit patterns
237    // returned above are the only finite values with a zero mantissa.
238    let tz = mantissa.trailing_zeros();
239    let Some(mantissa) = NonZeroU64::new(mantissa >> tz) else {
240        return Component::Zero;
241    };
242
243    Component::NonZero {
244        mantissa,
245        exponent: raw_exp + tz.cast_signed(),
246        is_negative: bits >> 63 != 0,
247    }
248}
249
250/// Parse an arbitrary `f64` into its exact IEEE 754 components.
251///
252/// Returns [`Component::Zero`] for ±0.0, or [`Component::NonZero`] with a
253/// non-zero mantissa where the value is exactly
254/// `(-1)^is_negative × mantissa × 2^exponent` and `mantissa` is odd (trailing
255/// zeros stripped).  See `REFERENCES.md` \[9-10\].
256///
257/// # Errors
258/// Returns [`LaError::NonFinite`] if `x` is NaN or infinite.
259#[cfg(test)]
260const fn decompose_f64(x: f64) -> Result<Component, LaError> {
261    let bits = x.to_bits();
262    let biased_exp = ((bits >> 52) & 0x7FF) as i32;
263
264    if biased_exp == 0x7FF {
265        cold_path();
266        return Err(LaError::non_finite_input_scalar());
267    }
268
269    Ok(decompose_proven_finite_f64(x))
270}
271
272/// Convert a [`BigInt`] × `2^exp` pair to a reduced [`BigRational`].
273///
274/// When `exp < 0` (denominator is `2^(-exp)`), shared factors of 2 are
275/// stripped from `value` to keep the fraction in lowest terms without a
276/// full GCD computation.
277fn big_int_exp_to_big_rational(mut value: BigInt, mut exp: i32) -> BigRational {
278    if value == BigInt::from(0) {
279        return BigRational::from_integer(BigInt::from(0));
280    }
281
282    // Strip shared powers of 2 between value and the 2^(-exp) denominator.
283    if exp < 0
284        && let Some(tz) = value.trailing_zeros()
285    {
286        let exp_abs = exp.unsigned_abs();
287        let reduce = tz.min(u64::from(exp_abs));
288        value >>= reduce;
289        let remaining_abs = u64::from(exp_abs) - reduce;
290        exp = negative_exponent_from_magnitude(remaining_abs);
291    }
292
293    if exp >= 0 {
294        BigRational::new_raw(value << exp.cast_unsigned(), BigInt::from(1u32))
295    } else {
296        BigRational::new_raw(value, BigInt::from(1u32) << exp.unsigned_abs())
297    }
298}
299
300/// Reconstruct a non-positive `i32` exponent from its unsigned magnitude.
301///
302/// The exact-conversion path can produce magnitude 2^31 for `i32::MIN`, which
303/// is one greater than `i32::MAX` and therefore cannot be converted before
304/// negation. Magnitudes derived from an `i32` never exceed that boundary.
305#[inline]
306fn negative_exponent_from_magnitude(magnitude: u64) -> i32 {
307    if magnitude == u64::from(i32::MIN.unsigned_abs()) {
308        return i32::MIN;
309    }
310
311    let Ok(value) = i32::try_from(magnitude) else {
312        cold_path();
313        unreachable!("negative exponent magnitude exceeds the i32 domain");
314    };
315    -value
316}
317
318/// Convert an exact rational result to `f64` only when the conversion is exact.
319///
320/// This supports the strict `*_exact_f64` public APIs by accepting only dyadic
321/// rational values that fit in finite binary64. The optional `index` is attached
322/// to [`LaError::Unrepresentable`] for vector-valued solve components.
323///
324/// # Errors
325/// Returns [`LaError::Unrepresentable`] with
326/// [`UnrepresentableReason::RequiresRounding`] when the rational denominator is
327/// not a power of two and the rounded value would still be finite.
328fn exact_rational_to_finite_f64(exact: &BigRational, index: Option<usize>) -> Result<f64, LaError> {
329    if exact.denom().sign() == Sign::NoSign {
330        cold_path();
331        return Err(LaError::unrepresentable(
332            index,
333            UnrepresentableReason::NotFinite,
334        ));
335    }
336
337    if exact.numer().sign() == Sign::NoSign {
338        return Ok(0.0);
339    }
340
341    let denominator = exact.denom();
342    if denominator.sign() == Sign::Plus
343        && let Some(denominator_exp) = positive_power_of_two_exponent(denominator)
344        && let Ok(denominator_exp) = i32::try_from(denominator_exp)
345    {
346        return big_int_exp_ref_to_finite_f64(exact.numer(), -denominator_exp, index, || {
347            rounded_rational_unrepresentable_reason(exact)
348        });
349    }
350
351    // `BigRational::new_raw` can expose a negative denominator or uncancelled
352    // common factors. Normalization is necessary before deciding whether the
353    // mathematical quotient is dyadic. Canonical dyadic values take the
354    // borrowed fast path above and do not clone.
355    let reduced = exact.reduced();
356    reduced_rational_to_finite_f64(&reduced, index)
357}
358
359/// Return `k` exactly when `value` is the positive integer `2^k`.
360fn positive_power_of_two_exponent(value: &BigInt) -> Option<u64> {
361    if value.sign() != Sign::Plus {
362        return None;
363    }
364
365    let exponent = value.trailing_zeros()?;
366    (value.bits().checked_sub(1) == Some(exponent)).then_some(exponent)
367}
368
369/// Strictly convert a reduced rational with a positive denominator.
370fn reduced_rational_to_finite_f64(
371    exact: &BigRational,
372    index: Option<usize>,
373) -> Result<f64, LaError> {
374    let Some(denominator_exp) = positive_power_of_two_exponent(exact.denom()) else {
375        cold_path();
376        return Err(LaError::unrepresentable(
377            index,
378            rounded_rational_unrepresentable_reason(exact),
379        ));
380    };
381    let Ok(denominator_exp) = i32::try_from(denominator_exp) else {
382        cold_path();
383        return Err(LaError::unrepresentable(
384            index,
385            rounded_rational_unrepresentable_reason(exact),
386        ));
387    };
388
389    big_int_exp_ref_to_finite_f64(exact.numer(), -denominator_exp, index, || {
390        rounded_rational_unrepresentable_reason(exact)
391    })
392}
393
394/// Classify a failed exact-rational-to-`f64` conversion by the rounded result.
395///
396/// Strict exact conversion has already failed when this helper is called. It
397/// preserves the [`UnrepresentableReason`] recovery contract: callers may retry
398/// with a rounded API only when that rounded result would still be finite.
399fn rounded_rational_unrepresentable_reason(exact: &BigRational) -> UnrepresentableReason {
400    match exact.to_f64() {
401        Some(value) if value.is_finite() => UnrepresentableReason::RequiresRounding,
402        _ => UnrepresentableReason::NotFinite,
403    }
404}
405
406/// Convert an exact rational result to a rounded finite `f64` using IEEE 754
407/// round-to-nearest, ties-to-even.
408fn exact_rational_to_rounded_f64(
409    exact: &BigRational,
410    index: Option<usize>,
411) -> Result<f64, LaError> {
412    if exact.denom().sign() == Sign::NoSign {
413        cold_path();
414        return Err(LaError::unrepresentable(
415            index,
416            UnrepresentableReason::NotFinite,
417        ));
418    }
419    if exact.numer().sign() == Sign::NoSign {
420        return Ok(0.0);
421    }
422
423    let Some(value) = exact.to_f64() else {
424        cold_path();
425        return Err(LaError::unrepresentable(
426            index,
427            UnrepresentableReason::NotFinite,
428        ));
429    };
430    if value.is_finite() {
431        Ok(value)
432    } else {
433        cold_path();
434        Err(LaError::unrepresentable(
435            index,
436            UnrepresentableReason::NotFinite,
437        ))
438    }
439}
440
441impl ExactF64Conversion for BigRational {
442    type Output = f64;
443
444    #[inline]
445    fn try_to_f64(&self) -> Result<Self::Output, LaError> {
446        exact_rational_to_finite_f64(self, None)
447    }
448
449    #[inline]
450    fn to_rounded_f64(&self) -> Result<Self::Output, LaError> {
451        exact_rational_to_rounded_f64(self, None)
452    }
453}
454
455impl<const D: usize> ExactF64Conversion for [BigRational; D] {
456    type Output = Vector<D>;
457
458    #[inline]
459    fn try_to_f64(&self) -> Result<Self::Output, LaError> {
460        let mut result = [0.0; D];
461        for (index, value) in self.iter().enumerate() {
462            result[index] = exact_rational_to_finite_f64(value, Some(index))?;
463        }
464        Vector::try_new(result)
465    }
466
467    #[inline]
468    fn to_rounded_f64(&self) -> Result<Self::Output, LaError> {
469        let mut result = [0.0; D];
470        for (index, value) in self.iter().enumerate() {
471            result[index] = exact_rational_to_rounded_f64(value, Some(index))?;
472        }
473        Vector::try_new(result)
474    }
475}
476
477/// Convert a `BigInt × 2^exp` pair to an exactly represented finite `f64`.
478///
479/// This avoids allocating a [`BigRational`] when determinant and solve paths
480/// already have an integer significand plus binary exponent. The optional
481/// `index` is forwarded to [`LaError::Unrepresentable`] for vector-valued solve
482/// components; determinant callers pass `None`.
483///
484/// # Errors
485/// Returns [`LaError::Unrepresentable`] with
486/// [`UnrepresentableReason::RequiresRounding`] when the exact nonzero value
487/// would need rounding or underflows below the smallest positive subnormal.
488///
489/// Returns [`LaError::Unrepresentable`] with
490/// [`UnrepresentableReason::NotFinite`] when the exact value cannot be
491/// represented by any finite `f64`.
492fn shifted_magnitude_to_u64(value: &BigInt, shift: u64) -> Option<u64> {
493    let word_bits = u64::from(u64::BITS);
494    let word_index = usize::try_from(shift / word_bits).ok()?;
495    let bit_shift = u32::try_from(shift % word_bits).ok()?;
496    let mut digits = value.iter_u64_digits().skip(word_index);
497    let low = digits.next()? >> bit_shift;
498    if bit_shift == 0 {
499        Some(low)
500    } else {
501        let high = digits.next().unwrap_or(0) << (u64::BITS - bit_shift);
502        Some(low | high)
503    }
504}
505
506/// Return whether a bit is set in the magnitude of `value`.
507fn magnitude_bit_is_set(value: &BigInt, bit: u64) -> bool {
508    let word_bits = u64::from(u64::BITS);
509    let Ok(word_index) = usize::try_from(bit / word_bits) else {
510        return false;
511    };
512    let bit_index = u32::try_from(bit % word_bits).unwrap_or(0);
513    value
514        .iter_u64_digits()
515        .nth(word_index)
516        .is_some_and(|word| word & (1_u64 << bit_index) != 0)
517}
518
519/// Return whether any magnitude bit below `exclusive_end` is set.
520fn magnitude_has_lower_bits(value: &BigInt, exclusive_end: u64) -> bool {
521    let word_bits = u64::from(u64::BITS);
522    let Ok(full_words) = usize::try_from(exclusive_end / word_bits) else {
523        return value.sign() != Sign::NoSign;
524    };
525    let partial_bits = u32::try_from(exclusive_end % word_bits).unwrap_or(0);
526    let mut digits = value.iter_u64_digits();
527
528    for _ in 0..full_words {
529        if digits.next().unwrap_or(0) != 0 {
530            return true;
531        }
532    }
533
534    if partial_bits == 0 {
535        false
536    } else {
537        let mask = (1_u64 << partial_bits) - 1;
538        digits.next().is_some_and(|word| word & mask != 0)
539    }
540}
541
542/// Right-shift a magnitude and round the retained integer to nearest-even.
543fn rounded_shifted_magnitude_to_u64(value: &BigInt, shift: u64) -> Option<u64> {
544    if shift > value.bits() {
545        return Some(0);
546    }
547    let retained = shifted_magnitude_to_u64(value, shift).unwrap_or(0);
548    if shift == 0 {
549        return Some(retained);
550    }
551
552    let guard_bit = shift - 1;
553    let increment = magnitude_bit_is_set(value, guard_bit)
554        && (magnitude_has_lower_bits(value, guard_bit) || retained & 1 != 0);
555    retained.checked_add(u64::from(increment))
556}
557
558/// Classify an inexact integer conversion, evaluating rounding only in the
559/// maximum exponent bin where it can overflow to infinity.
560#[inline]
561fn inexact_big_int_reason(
562    top_bit_exp: i64,
563    rounded_reason: impl FnOnce() -> UnrepresentableReason,
564) -> UnrepresentableReason {
565    if top_bit_exp < F64_MAX_BINARY_EXPONENT {
566        UnrepresentableReason::RequiresRounding
567    } else {
568        rounded_reason()
569    }
570}
571
572/// Round a `BigInt × 2^exp` pair directly to finite binary64.
573///
574/// The implementation reads only the magnitude bits needed for the binary64
575/// significand and rounding decision. It therefore avoids constructing a
576/// potentially enormous [`BigRational`] denominator for very negative
577/// exponents.
578fn big_int_exp_ref_to_rounded_f64(
579    value: &BigInt,
580    exp: i32,
581    index: Option<usize>,
582) -> Result<f64, LaError> {
583    if value.sign() == Sign::NoSign {
584        return Ok(0.0);
585    }
586
587    let sign = if value.sign() == Sign::Minus {
588        1_u64 << 63
589    } else {
590        0
591    };
592    let Ok(bit_len) = i64::try_from(value.bits()) else {
593        cold_path();
594        return Err(LaError::unrepresentable(
595            index,
596            UnrepresentableReason::NotFinite,
597        ));
598    };
599    let Some(mut top_bit_exp) = i64::from(exp).checked_add(bit_len - 1) else {
600        cold_path();
601        return Err(LaError::unrepresentable(
602            index,
603            UnrepresentableReason::NotFinite,
604        ));
605    };
606    if top_bit_exp > F64_MAX_BINARY_EXPONENT {
607        cold_path();
608        return Err(LaError::unrepresentable(
609            index,
610            UnrepresentableReason::NotFinite,
611        ));
612    }
613
614    if top_bit_exp >= F64_MIN_NORMAL_EXPONENT {
615        let mut significand = if bit_len <= F64_SIGNIFICAND_BITS {
616            let Some(magnitude) = shifted_magnitude_to_u64(value, 0) else {
617                cold_path();
618                unreachable!("nonzero integer must expose magnitude digits");
619            };
620            let shift = u32::try_from(F64_SIGNIFICAND_BITS - bit_len)
621                .unwrap_or_else(|_| unreachable!("normal significand shift must fit u32"));
622            magnitude
623                .checked_shl(shift)
624                .unwrap_or_else(|| unreachable!("normal significand must fit u64"))
625        } else {
626            let shift = u64::try_from(bit_len - F64_SIGNIFICAND_BITS)
627                .unwrap_or_else(|_| unreachable!("positive significand shift must fit u64"));
628            rounded_shifted_magnitude_to_u64(value, shift)
629                .unwrap_or_else(|| unreachable!("rounded binary64 significand must fit u64"))
630        };
631
632        if significand == 1_u64 << F64_SIGNIFICAND_BITS {
633            significand >>= 1;
634            top_bit_exp += 1;
635        }
636        if top_bit_exp > F64_MAX_BINARY_EXPONENT {
637            cold_path();
638            return Err(LaError::unrepresentable(
639                index,
640                UnrepresentableReason::NotFinite,
641            ));
642        }
643
644        let biased_exp = u64::try_from(top_bit_exp + F64_EXPONENT_BIAS)
645            .unwrap_or_else(|_| unreachable!("normal exponent must be positive"));
646        return Ok(f64::from_bits(
647            sign | (biased_exp << F64_FRACTION_BITS) | (significand & F64_FRACTION_MASK),
648        ));
649    }
650
651    let subnormal_shift = i64::from(exp) - F64_MIN_BINARY_EXPONENT;
652    let significand = if subnormal_shift >= 0 {
653        let Some(magnitude) = shifted_magnitude_to_u64(value, 0) else {
654            cold_path();
655            unreachable!("nonzero integer must expose magnitude digits");
656        };
657        let shift = u32::try_from(subnormal_shift)
658            .unwrap_or_else(|_| unreachable!("subnormal left shift must fit u32"));
659        magnitude
660            .checked_shl(shift)
661            .unwrap_or_else(|| unreachable!("subnormal significand must fit u64"))
662    } else {
663        let shift = u64::try_from(-subnormal_shift)
664            .unwrap_or_else(|_| unreachable!("subnormal right shift must fit u64"));
665        rounded_shifted_magnitude_to_u64(value, shift)
666            .unwrap_or_else(|| unreachable!("rounded subnormal significand must fit u64"))
667    };
668
669    if significand == 1_u64 << F64_FRACTION_BITS {
670        return Ok(f64::from_bits(sign | (1_u64 << F64_FRACTION_BITS)));
671    }
672    Ok(f64::from_bits(sign | significand))
673}
674
675/// Borrowed core for exact integer-and-exponent conversion.
676///
677/// The normalized significand is read directly from the [`BigInt`] digits, so
678/// successful strict conversion does not clone an already-computed exact
679/// result. `rounded_reason` is evaluated only when finite output would require
680/// rounding.
681fn big_int_exp_ref_to_finite_f64(
682    value: &BigInt,
683    exp: i32,
684    index: Option<usize>,
685    rounded_reason: impl FnOnce() -> UnrepresentableReason,
686) -> Result<f64, LaError> {
687    if value.sign() == Sign::NoSign {
688        return Ok(0.0);
689    }
690
691    let is_negative = value.sign() == Sign::Minus;
692    let mut exp = i64::from(exp);
693    let Some(trailing_zeros) = value.trailing_zeros() else {
694        cold_path();
695        unreachable!("nonzero integer must have a least-significant set bit");
696    };
697    let Ok(trailing_zeros_i64) = i64::try_from(trailing_zeros) else {
698        cold_path();
699        return Err(LaError::unrepresentable(
700            index,
701            UnrepresentableReason::NotFinite,
702        ));
703    };
704    let Some(updated_exp) = exp.checked_add(trailing_zeros_i64) else {
705        cold_path();
706        return Err(LaError::unrepresentable(
707            index,
708            UnrepresentableReason::NotFinite,
709        ));
710    };
711    exp = updated_exp;
712
713    let Some(bit_len) = value.bits().checked_sub(trailing_zeros) else {
714        cold_path();
715        unreachable!("trailing-zero count cannot exceed integer bit length");
716    };
717    let Ok(bit_len) = i64::try_from(bit_len) else {
718        cold_path();
719        return Err(LaError::unrepresentable(
720            index,
721            UnrepresentableReason::NotFinite,
722        ));
723    };
724    let Some(top_bit_exp) = exp.checked_add(bit_len - 1) else {
725        cold_path();
726        return Err(LaError::unrepresentable(
727            index,
728            UnrepresentableReason::NotFinite,
729        ));
730    };
731    if top_bit_exp > F64_MAX_BINARY_EXPONENT {
732        cold_path();
733        return Err(LaError::unrepresentable(
734            index,
735            UnrepresentableReason::NotFinite,
736        ));
737    }
738    if exp < F64_MIN_BINARY_EXPONENT {
739        cold_path();
740        // A low least-significant exponent normally rounds to a finite value,
741        // but a very wide integer in the maximum exponent bin can round up to
742        // infinity.
743        let reason = inexact_big_int_reason(top_bit_exp, rounded_reason);
744        return Err(LaError::unrepresentable(index, reason));
745    }
746    if bit_len > F64_SIGNIFICAND_BITS {
747        cold_path();
748        // Rounding can overflow only when the exact value already occupies the
749        // maximum binary64 exponent bin. Avoid the full rounding calculation
750        // for every ordinary inexact conversion.
751        let reason = inexact_big_int_reason(top_bit_exp, rounded_reason);
752        return Err(LaError::unrepresentable(index, reason));
753    }
754
755    let Some(mantissa) = shifted_magnitude_to_u64(value, trailing_zeros) else {
756        cold_path();
757        return Err(LaError::unrepresentable(
758            index,
759            UnrepresentableReason::NotFinite,
760        ));
761    };
762    let sign = if is_negative { 1u64 << 63 } else { 0 };
763
764    if top_bit_exp < F64_MIN_NORMAL_EXPONENT {
765        let Ok(shift) = u32::try_from(exp - F64_MIN_BINARY_EXPONENT) else {
766            cold_path();
767            return Err(LaError::unrepresentable(
768                index,
769                UnrepresentableReason::RequiresRounding,
770            ));
771        };
772        Ok(f64::from_bits(sign | (mantissa << shift)))
773    } else {
774        let Ok(biased_exp) = u64::try_from(top_bit_exp + F64_EXPONENT_BIAS) else {
775            cold_path();
776            return Err(LaError::unrepresentable(
777                index,
778                UnrepresentableReason::NotFinite,
779            ));
780        };
781        let Ok(shift) = u32::try_from(F64_FRACTION_BITS - (bit_len - 1)) else {
782            cold_path();
783            return Err(LaError::unrepresentable(
784                index,
785                UnrepresentableReason::RequiresRounding,
786            ));
787        };
788        let significand = mantissa << shift;
789        Ok(f64::from_bits(
790            sign | (biased_exp << F64_FRACTION_BITS) | (significand & F64_FRACTION_MASK),
791        ))
792    }
793}
794
795fn big_int_exp_to_finite_f64(
796    value: &BigInt,
797    exp: i32,
798    index: Option<usize>,
799) -> Result<f64, LaError> {
800    big_int_exp_ref_to_finite_f64(value, exp, index, || {
801        match big_int_exp_ref_to_rounded_f64(value, exp, index) {
802            Ok(_) => UnrepresentableReason::RequiresRounding,
803            Err(_) => UnrepresentableReason::NotFinite,
804        }
805    })
806}
807
808/// Convert a `BigInt × 2^exp` determinant pair to a rounded finite `f64`.
809fn big_int_exp_to_rounded_f64(value: &BigInt, exp: i32) -> Result<f64, LaError> {
810    big_int_exp_ref_to_rounded_f64(value, exp, None)
811}
812
813// -----------------------------------------------------------------------
814// Shared integer-scaling and Bareiss primitives
815// -----------------------------------------------------------------------
816//
817// Both `exact_det_int_finite` (determinants) and `bareiss_solve_finite` (linear
818// systems) parse every f64 entry into a proof-bearing component, track the
819// minimum exponent across non-zero entries, and scale each entry by
820// `2^(exp − e_min)`. Determinants then use direct expansions for D≤4 and
821// fraction-free Bareiss elimination for D≥5. Solves derive matrix and RHS scales
822// independently, share the lower scale when their gap is at most 64 bits, use
823// Bareiss elimination on the augmented system, and restore the selected scales'
824// exact power-of-two ratio after rational back-substitution.
825
826/// Decomposed finite f64 in the form `(-1)^is_negative · mantissa · 2^exponent`.
827///
828/// `Zero` represents ±0.0. Non-zero entries carry a [`NonZeroU64`] mantissa, so
829/// the exact-arithmetic paths cannot accidentally combine an absent mantissa
830/// with active exponent/sign fields after decomposition.
831#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
832enum Component {
833    #[default]
834    Zero,
835    NonZero {
836        mantissa: NonZeroU64,
837        exponent: i32,
838        is_negative: bool,
839    },
840}
841
842impl Component {
843    /// Return the exponent carried by a non-zero component.
844    const fn exponent(self) -> Option<i32> {
845        match self {
846            Self::Zero => None,
847            Self::NonZero { exponent, .. } => Some(exponent),
848        }
849    }
850}
851
852mod decomposition {
853    use super::Component;
854
855    /// A component collection paired with its proven minimum non-zero exponent.
856    ///
857    /// `None` represents an all-zero collection; no sentinel exponent is stored.
858    /// Private fields prevent callers from supplying components and their proof
859    /// independently.
860    #[derive(Clone, Debug, Eq, PartialEq)]
861    pub(super) struct Decomposed<T> {
862        components: T,
863        min_exponent: Option<i32>,
864    }
865
866    impl<T> Decomposed<T> {
867        /// Borrow the parsed components.
868        pub(super) const fn components(&self) -> &T {
869            &self.components
870        }
871
872        /// Return the minimum exponent, or `None` when every component is zero.
873        pub(super) const fn min_exponent(&self) -> Option<i32> {
874            self.min_exponent
875        }
876    }
877
878    impl<const D: usize> Decomposed<[Component; D]> {
879        /// Derive a vector decomposition and its proof together.
880        pub(super) fn from_vector_components(components: [Component; D]) -> Self {
881            let min_exponent = components
882                .iter()
883                .filter_map(|component| component.exponent())
884                .min();
885            Self {
886                components,
887                min_exponent,
888            }
889        }
890    }
891
892    impl<const D: usize> Decomposed<[[Component; D]; D]> {
893        /// Derive a matrix decomposition and its proof together.
894        pub(super) fn from_matrix_components(components: [[Component; D]; D]) -> Self {
895            let min_exponent = components
896                .iter()
897                .flatten()
898                .filter_map(|component| component.exponent())
899                .min();
900            Self {
901                components,
902                min_exponent,
903            }
904        }
905    }
906
907    /// A scaling exponent derived from a component collection's minimum.
908    ///
909    /// The private field prevents raw construction outside this proof-owning
910    /// module.
911    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
912    pub(super) struct ScaleExponent {
913        value: i32,
914    }
915
916    impl ScaleExponent {
917        /// Canonical scale for an empty or all-zero component collection.
918        pub(super) const ZERO: Self = Self { value: 0 };
919
920        /// Select a scale for one decomposed collection.
921        pub(super) const fn for_decomposed<T>(decomposed: &Decomposed<T>) -> Self {
922            let value = match decomposed.min_exponent() {
923                Some(exponent) => exponent,
924                None => 0,
925            };
926            Self { value }
927        }
928
929        /// Select the lower of two already-derived collection scales.
930        pub(super) const fn min(self, other: Self) -> Self {
931            if self.value < other.value {
932                self
933            } else {
934                other
935            }
936        }
937
938        /// Return the proven collection exponent.
939        pub(super) const fn get(self) -> i32 {
940            self.value
941        }
942
943        /// Compute a non-negative shift from this proven collection exponent.
944        ///
945        /// # Panics
946        /// Panics only if a private decomposition invariant is broken and an entry
947        /// exponent is lower than the common minimum.
948        pub(super) fn shift_for(self, exponent: i32) -> u32 {
949            let Some(shift) = exponent.checked_sub(self.value) else {
950                unreachable!("finite f64 exponent difference cannot overflow");
951            };
952            let Ok(shift) = u32::try_from(shift) else {
953                unreachable!("scale exponent cannot exceed a component exponent");
954            };
955            shift
956        }
957    }
958}
959
960use decomposition::{Decomposed, ScaleExponent};
961
962/// Decompose a matrix whose finite-storage invariant has already been proven.
963fn decompose_proven_finite_matrix<const D: usize>(
964    m: &Matrix<D>,
965) -> Decomposed<[[Component; D]; D]> {
966    let components =
967        from_fn(|row| from_fn(|col| decompose_proven_finite_f64(m.as_rows()[row][col])));
968    Decomposed::from_matrix_components(components)
969}
970
971/// Decompose a vector whose finite-storage invariant has already been proven.
972fn decompose_proven_finite_vector<const D: usize>(v: &Vector<D>) -> Decomposed<[Component; D]> {
973    let components = from_fn(|index| decompose_proven_finite_f64(v.as_array()[index]));
974    Decomposed::from_vector_components(components)
975}
976
977/// Convert a single decomposed component to its scaled `BigInt`
978/// representation: `(±mantissa) << (exp − e_min)`.
979#[inline]
980fn component_to_big_int(component: Component, scale: ScaleExponent) -> BigInt {
981    match component {
982        Component::Zero => BigInt::from(0),
983        Component::NonZero {
984            mantissa,
985            exponent,
986            is_negative,
987        } => {
988            let value = BigInt::from(mantissa.get()) << scale.shift_for(exponent);
989            if is_negative { -value } else { value }
990        }
991    }
992}
993
994/// Build a `D×D` integer matrix from components, scaled to a shared base.
995fn build_big_int_matrix<const D: usize>(
996    components: &[[Component; D]; D],
997    scale: ScaleExponent,
998) -> [[BigInt; D]; D] {
999    from_fn(|row| from_fn(|col| component_to_big_int(components[row][col], scale)))
1000}
1001
1002/// Build a length-`D` integer vector from components, scaled to a shared base.
1003fn build_big_int_vec<const D: usize>(
1004    components: &[Component; D],
1005    scale: ScaleExponent,
1006) -> [BigInt; D] {
1007    from_fn(|index| component_to_big_int(components[index], scale))
1008}
1009
1010/// Compute a 2×2 determinant from a scaled integer matrix.
1011#[inline]
1012fn det2_big_int<const D: usize>(a: &[[BigInt; D]; D]) -> BigInt {
1013    &a[0][0] * &a[1][1] - &a[0][1] * &a[1][0]
1014}
1015
1016/// Compute an exact 3×3 determinant from borrowed scaled-integer entries.
1017///
1018/// This fixed-shape kernel serves both the direct D=3 determinant path and the
1019/// 3×3 minors used by the D=4 expansion. Borrowing entries avoids cloning
1020/// [`BigInt`] values while keeping every operation in exact integer arithmetic.
1021#[inline]
1022fn det3_big_int_entries(a: [[&BigInt; 3]; 3]) -> BigInt {
1023    let m00 = a[1][1] * a[2][2] - a[1][2] * a[2][1];
1024    let m01 = a[1][0] * a[2][2] - a[1][2] * a[2][0];
1025    let m02 = a[1][0] * a[2][1] - a[1][1] * a[2][0];
1026    a[0][0] * m00 - a[0][1] * m01 + a[0][2] * m02
1027}
1028
1029/// Compute a 3×3 determinant from a scaled integer matrix.
1030#[inline]
1031fn det3_big_int<const D: usize>(a: &[[BigInt; D]; D]) -> BigInt {
1032    det3_big_int_entries([
1033        [&a[0][0], &a[0][1], &a[0][2]],
1034        [&a[1][0], &a[1][1], &a[1][2]],
1035        [&a[2][0], &a[2][1], &a[2][2]],
1036    ])
1037}
1038
1039/// Compute a 4×4 determinant from a scaled integer matrix.
1040#[inline]
1041fn det4_big_int<const D: usize>(a: &[[BigInt; D]; D]) -> BigInt {
1042    let mut det = BigInt::from(0);
1043
1044    if a[0][0].sign() != Sign::NoSign {
1045        let c00 = det3_big_int_entries([
1046            [&a[1][1], &a[1][2], &a[1][3]],
1047            [&a[2][1], &a[2][2], &a[2][3]],
1048            [&a[3][1], &a[3][2], &a[3][3]],
1049        ]);
1050        det += &a[0][0] * c00;
1051    }
1052    if a[0][1].sign() != Sign::NoSign {
1053        let c01 = det3_big_int_entries([
1054            [&a[1][0], &a[1][2], &a[1][3]],
1055            [&a[2][0], &a[2][2], &a[2][3]],
1056            [&a[3][0], &a[3][2], &a[3][3]],
1057        ]);
1058        det -= &a[0][1] * c01;
1059    }
1060    if a[0][2].sign() != Sign::NoSign {
1061        let c02 = det3_big_int_entries([
1062            [&a[1][0], &a[1][1], &a[1][3]],
1063            [&a[2][0], &a[2][1], &a[2][3]],
1064            [&a[3][0], &a[3][1], &a[3][3]],
1065        ]);
1066        det += &a[0][2] * c02;
1067    }
1068    if a[0][3].sign() != Sign::NoSign {
1069        let c03 = det3_big_int_entries([
1070            [&a[1][0], &a[1][1], &a[1][2]],
1071            [&a[2][0], &a[2][1], &a[2][2]],
1072            [&a[3][0], &a[3][1], &a[3][2]],
1073        ]);
1074        det -= &a[0][3] * c03;
1075    }
1076
1077    det
1078}
1079
1080/// Outcome of a Bareiss forward-elimination pass.
1081#[derive(Debug)]
1082enum BareissResult {
1083    /// Elimination completed; `odd_swaps` records the parity of row
1084    /// swaps (relevant for determinants; solves discard it).
1085    Upper { odd_swaps: bool },
1086    /// Column `pivot_col` has no non-zero pivot at or below its diagonal.
1087    Singular { pivot_col: usize },
1088}
1089
1090/// Run Bareiss fraction-free forward elimination on the `D×D` integer
1091/// matrix `a`, optionally augmented with a length-`D` RHS vector.
1092///
1093/// When `rhs` is `Some`, row swaps and the inner-loop Bareiss update are
1094/// mirrored on the RHS (treating it as column `D+1` of an augmented
1095/// system).  On return, `a` is upper triangular and the last pivot lives
1096/// in `a[D-1][D-1]`.
1097///
1098/// First-non-zero pivoting is used: since all arithmetic is exact, any
1099/// non-zero pivot is valid — no tolerance is required.
1100fn bareiss_forward_eliminate<const D: usize>(
1101    a: &mut [[BigInt; D]; D],
1102    mut rhs: Option<&mut [BigInt; D]>,
1103) -> BareissResult {
1104    let zero = BigInt::from(0);
1105    let mut prev_pivot = BigInt::from(1);
1106    let mut odd_swaps = false;
1107
1108    for k in 0..D {
1109        // First-non-zero pivot search.
1110        if a[k][k] == zero {
1111            let mut found = false;
1112            for i in (k + 1)..D {
1113                if a[i][k] != zero {
1114                    a.swap(k, i);
1115                    if let Some(r) = &mut rhs {
1116                        r.swap(k, i);
1117                    }
1118                    odd_swaps = !odd_swaps;
1119                    found = true;
1120                    break;
1121                }
1122            }
1123            if !found {
1124                cold_path();
1125                return BareissResult::Singular { pivot_col: k };
1126            }
1127        }
1128
1129        // The final pivot has now been proven non-zero. There are no rows or
1130        // columns left to eliminate, and `prev_pivot` would never be read again.
1131        if k + 1 == D {
1132            break;
1133        }
1134
1135        // Elimination.  The Bareiss update reads the current `a[i][k]`
1136        // in both the inner `j`-loop and the RHS update, so zero it only
1137        // *after* those reads.
1138        for i in (k + 1)..D {
1139            for j in (k + 1)..D {
1140                a[i][j] = (&a[k][k] * &a[i][j] - &a[i][k] * &a[k][j]) / &prev_pivot;
1141            }
1142            if let Some(r) = &mut rhs {
1143                r[i] = (&a[k][k] * &r[i] - &a[i][k] * &r[k]) / &prev_pivot;
1144            }
1145            a[i][k].clone_from(&zero);
1146        }
1147
1148        prev_pivot.clone_from(&a[k][k]);
1149    }
1150
1151    // Post-conditions (debug builds only): `a` is upper triangular with
1152    // non-zero pivots.  These catch future regressions in the inner-loop
1153    // update or pivot-search logic without runtime cost in release.
1154    #[cfg(debug_assertions)]
1155    for (k, row) in a.iter().enumerate() {
1156        assert_ne!(row[k], zero, "pivot at ({k}, {k}) must be non-zero");
1157        for (i, lower_row) in a.iter().enumerate().skip(k + 1) {
1158            assert_eq!(
1159                lower_row[k], zero,
1160                "sub-diagonal at ({i}, {k}) must be zero"
1161            );
1162        }
1163    }
1164
1165    BareissResult::Upper { odd_swaps }
1166}
1167
1168/// Compute the determinant scale exponent `D × e_min`.
1169///
1170/// This centralizes the scale-overflow classification used by exact
1171/// determinant value APIs. Sign-only evaluation deliberately bypasses this
1172/// bookkeeping because a positive binary scale cannot change determinant sign.
1173///
1174/// # Errors
1175/// Returns [`LaError::DeterminantScaleOverflow`] if `D` cannot fit in the
1176/// internal `i32` exponent multiplier or if `D × e_min` overflows `i32`.
1177fn determinant_scale_exp<const D: usize>(e_min: i32) -> Result<i32, LaError> {
1178    let Ok(d_i32) = i32::try_from(D) else {
1179        cold_path();
1180        return Err(LaError::determinant_scale_overflow(D, e_min));
1181    };
1182    let Some(total_exp) = e_min.checked_mul(d_i32) else {
1183        cold_path();
1184        return Err(LaError::determinant_scale_overflow(D, e_min));
1185    };
1186    Ok(total_exp)
1187}
1188
1189/// Compute the determinant integer and its shared per-entry scale.
1190///
1191/// Returns `(det_int, scale)` where the true determinant is
1192/// `det_int × 2^(D × scale)`. Since that scale factor is always positive,
1193/// callers interested only in the sign do not need to form `D × scale`.
1194///
1195/// All arithmetic is in `BigInt` — no `BigRational`, no GCD, no denominator
1196/// tracking.  Each f64 entry is decomposed into `mantissa × 2^exponent` and
1197/// scaled to a common base `2^e_min` so every entry becomes an integer. D≤4
1198/// uses direct determinant expansions; larger matrices use Bareiss elimination
1199/// whose inner-loop division is exact (guaranteed by the algorithm).
1200///
1201fn scaled_det_int_finite<const D: usize>(m: &Matrix<D>) -> (BigInt, ScaleExponent) {
1202    let decomposed = decompose_proven_finite_matrix(m);
1203    scaled_det_int_decomposed(&decomposed)
1204}
1205
1206/// Compute a determinant integer from a proof-bearing component table.
1207fn scaled_det_int_decomposed<const D: usize>(
1208    decomposed: &Decomposed<[[Component; D]; D]>,
1209) -> (BigInt, ScaleExponent) {
1210    // D == 0 has no `a[D-1][D-1]` to read; shortcut to the empty-product
1211    // determinant.
1212    if D == 0 {
1213        return (BigInt::from(1), ScaleExponent::ZERO);
1214    }
1215
1216    if decomposed.min_exponent().is_none() {
1217        return (BigInt::from(0), ScaleExponent::ZERO);
1218    }
1219    let scale = ScaleExponent::for_decomposed(decomposed);
1220    let mut a = build_big_int_matrix(decomposed.components(), scale);
1221    let det_int = match D {
1222        1 => take(&mut a[0][0]),
1223        2 => det2_big_int(&a),
1224        3 => det3_big_int(&a),
1225        4 => det4_big_int(&a),
1226        _ => {
1227            let odd_swaps = match bareiss_forward_eliminate(&mut a, None) {
1228                BareissResult::Upper { odd_swaps } => odd_swaps,
1229                BareissResult::Singular { .. } => {
1230                    cold_path();
1231                    return (BigInt::from(0), ScaleExponent::ZERO);
1232                }
1233            };
1234
1235            let det = take(&mut a[D - 1][D - 1]);
1236            if odd_swaps { -det } else { det }
1237        }
1238    };
1239
1240    (det_int, scale)
1241}
1242
1243/// Compute the exact determinant as an integer plus one total binary scale.
1244///
1245/// Zero determinants use exponent zero because their value is independent of
1246/// scale. Non-zero determinants validate `D × e_min` for the value-producing
1247/// exact APIs; sign-only callers use [`scaled_det_int_finite`] directly.
1248fn exact_det_int_finite<const D: usize>(m: &Matrix<D>) -> Result<(BigInt, i32), LaError> {
1249    let (det_int, scale) = scaled_det_int_finite(m);
1250    if det_int.sign() == Sign::NoSign {
1251        return Ok((det_int, 0));
1252    }
1253    let total_exp = determinant_scale_exp::<D>(scale.get())?;
1254    Ok((det_int, total_exp))
1255}
1256
1257/// Compute the exact determinant of a `D×D` matrix using direct `BigInt`
1258/// expansions for D≤4 or integer-only Bareiss elimination for D≥5, then return
1259/// the result as a `BigRational`.
1260fn exact_det_finite<const D: usize>(m: &Matrix<D>) -> Result<BigRational, LaError> {
1261    let (det_int, total_exp) = exact_det_int_finite(m)?;
1262    Ok(big_int_exp_to_big_rational(det_int, total_exp))
1263}
1264
1265/// Solve `A x = b` exactly after matrix and RHS finiteness has been proven.
1266///
1267/// Public [`Matrix`] / [`Vector`] values are finite by construction before
1268/// reaching this helper, so decomposition can proceed without rediscovering
1269/// stored NaN/∞ entries.
1270///
1271/// # Errors
1272/// Returns [`LaError::Singular`] if the matrix is exactly singular.
1273fn bareiss_solve_finite<const D: usize>(
1274    m: &Matrix<D>,
1275    b: &Vector<D>,
1276) -> Result<[BigRational; D], LaError> {
1277    let matrix = decompose_proven_finite_matrix(m);
1278    let rhs = decompose_proven_finite_vector(b);
1279    bareiss_solve_components(&matrix, &rhs)
1280}
1281
1282/// Solve an exact integer-scaled augmented system from decomposed components.
1283///
1284/// Forward elimination runs in [`BigInt`] using fraction-free Bareiss updates
1285/// \[7\]. This is exact arithmetic, so there is no floating-point conditioning or
1286/// roundoff error in the elimination itself; ill-conditioned inputs can still
1287/// produce large exact numerators and denominators in the final solution. The
1288/// elimination phase performs `O(D³)` integer operations and Bareiss exact
1289/// division controls intermediate integer growth compared with naive fraction
1290/// arithmetic. The resulting upper-triangular system is then lifted into
1291/// [`BigRational`] for back-substitution, limiting rational arithmetic to the
1292/// `O(D²)` phase.
1293///
1294/// # Errors
1295/// Returns [`LaError::Singular`] if the matrix component table represents an
1296/// exactly singular matrix.
1297fn bareiss_solve_components<const D: usize>(
1298    matrix: &Decomposed<[[Component; D]; D]>,
1299    rhs: &Decomposed<[Component; D]>,
1300) -> Result<[BigRational; D], LaError> {
1301    const MAX_SHARED_SCALE_GAP_BITS: u32 = 64;
1302
1303    let independent_matrix_scale = ScaleExponent::for_decomposed(matrix);
1304    let independent_rhs_scale = ScaleExponent::for_decomposed(rhs);
1305    let scale_gap = independent_matrix_scale
1306        .get()
1307        .abs_diff(independent_rhs_scale.get());
1308    let (matrix_scale, rhs_scale) = if scale_gap <= MAX_SHARED_SCALE_GAP_BITS {
1309        let shared = independent_matrix_scale.min(independent_rhs_scale);
1310        (shared, shared)
1311    } else {
1312        (independent_matrix_scale, independent_rhs_scale)
1313    };
1314    let mut a = build_big_int_matrix(matrix.components(), matrix_scale);
1315    let mut rhs = build_big_int_vec(rhs.components(), rhs_scale);
1316
1317    match bareiss_forward_eliminate(&mut a, Some(&mut rhs)) {
1318        BareissResult::Upper { .. } => {}
1319        BareissResult::Singular { pivot_col } => {
1320            cold_path();
1321            return Err(LaError::singular_exact(pivot_col));
1322        }
1323    }
1324
1325    let mut x: [BigRational; D] = from_fn(|_| BigRational::from_integer(BigInt::from(0)));
1326    for i in (0..D).rev() {
1327        let mut sum = BigRational::from_integer(take(&mut rhs[i]));
1328        for j in (i + 1)..D {
1329            let a_ij = BigRational::from_integer(take(&mut a[i][j]));
1330            sum -= &a_ij * &x[j];
1331        }
1332        let a_ii = BigRational::from_integer(take(&mut a[i][i]));
1333        x[i] = sum / &a_ii;
1334    }
1335
1336    let solution_scale_exp = rhs_scale
1337        .get()
1338        .checked_sub(matrix_scale.get())
1339        .unwrap_or_else(|| unreachable!("finite f64 scale difference cannot overflow i32"));
1340    if solution_scale_exp != 0 {
1341        let solution_scale = big_int_exp_to_big_rational(BigInt::from(1_u8), solution_scale_exp);
1342        for component in &mut x {
1343            *component *= &solution_scale;
1344        }
1345    }
1346
1347    Ok(x)
1348}
1349
1350/// Exact determinant converted to finite `f64` without rounding.
1351///
1352/// This preserves the strict contract of [`Matrix::det_exact_f64`]: if the exact
1353/// determinant is not representable as a finite binary64 value, callers receive
1354/// a typed [`LaError::Unrepresentable`] instead of a rounded result.
1355///
1356/// # Errors
1357/// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
1358/// overflows the internal exponent representation.
1359///
1360/// Returns [`LaError::Unrepresentable`] with
1361/// [`UnrepresentableReason::RequiresRounding`] when the exact determinant is
1362/// finite but not exactly representable as binary64, or
1363/// [`UnrepresentableReason::NotFinite`] when no finite `f64` can represent it.
1364#[inline]
1365fn det_exact_f64_finite<const D: usize>(m: &Matrix<D>) -> Result<f64, LaError> {
1366    let (det_int, total_exp) = exact_det_int_finite(m)?;
1367    big_int_exp_to_finite_f64(&det_int, total_exp, None)
1368}
1369
1370/// Exact determinant rounded to finite `f64`.
1371///
1372/// This is the intentionally lossy counterpart to [`det_exact_f64_finite`] and
1373/// the private implementation target for [`Matrix::det_exact_rounded_f64`].
1374///
1375/// # Errors
1376/// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
1377/// overflows the internal exponent representation.
1378///
1379/// Returns [`LaError::Unrepresentable`] with
1380/// [`UnrepresentableReason::NotFinite`] if rounding cannot produce a finite `f64`.
1381#[inline]
1382fn det_exact_rounded_f64_finite<const D: usize>(m: &Matrix<D>) -> Result<f64, LaError> {
1383    let (det_int, total_exp) = exact_det_int_finite(m)?;
1384    big_int_exp_to_rounded_f64(&det_int, total_exp)
1385}
1386
1387/// Exact determinant sign for an already finite matrix.
1388///
1389/// The fast `f64` filter treats overflowed or underflow-sensitive scalar
1390/// intermediates as inconclusive, then falls back to exact integer sign
1391/// computation: direct expansion for D≤4 or Bareiss elimination for D≥5.
1392///
1393#[inline]
1394fn det_sign_exact_finite<const D: usize>(m: &Matrix<D>) -> DeterminantSign {
1395    if let Ok(Some(estimate)) = m.det_direct_with_errbound() {
1396        let det_f64 = estimate.determinant();
1397        let error_bound = estimate.absolute_error_bound();
1398        if det_f64 > error_bound {
1399            return DeterminantSign::Positive;
1400        }
1401        if det_f64 < -error_bound {
1402            return DeterminantSign::Negative;
1403        }
1404    }
1405
1406    cold_path();
1407    let decomposed = decompose_proven_finite_matrix(m);
1408    let (det_int, _) = scaled_det_int_decomposed(&decomposed);
1409    match det_int.sign() {
1410        Sign::Plus => DeterminantSign::Positive,
1411        Sign::Minus => DeterminantSign::Negative,
1412        Sign::NoSign => DeterminantSign::Zero,
1413    }
1414}
1415
1416impl<const D: usize> Matrix<D> {
1417    /// Exact determinant using arbitrary-precision rational arithmetic.
1418    ///
1419    /// Requires the `exact` Cargo feature.
1420    ///
1421    /// Returns the determinant as an exact [`BigRational`] value. Every finite
1422    /// `f64` is exactly representable as a rational, so the conversion is
1423    /// lossless and the result is exact for the stored binary64 entries. It
1424    /// cannot recover precision lost before matrix construction.
1425    ///
1426    /// # When to use
1427    ///
1428    /// Use this when you need the exact determinant *value* — for example,
1429    /// volume computation over stored coordinates or distinguishing simplices
1430    /// that are exactly degenerate at those coordinates from near-degenerate
1431    /// ones. If you only need the *sign*, prefer
1432    /// [`det_sign_exact`](Self::det_sign_exact) which has a fast f64 filter.
1433    ///
1434    /// # Examples
1435    /// ```
1436    /// use la_stack::prelude::*;
1437    ///
1438    /// # fn main() -> Result<(), LaError> {
1439    /// let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
1440    /// let det = m.det_exact()?;
1441    /// // det = 1*4 - 2*3 = -2  (exact)
1442    /// assert_eq!(det, BigRational::from_integer((-2).into()));
1443    /// # Ok(())
1444    /// # }
1445    /// ```
1446    ///
1447    /// # Errors
1448    /// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
1449    /// overflows the internal exponent representation.
1450    #[inline]
1451    pub fn det_exact(&self) -> Result<BigRational, LaError> {
1452        exact_det_finite(self)
1453    }
1454
1455    /// Exact determinant converted to `f64`.
1456    ///
1457    /// Requires the `exact` Cargo feature.
1458    ///
1459    /// Computes the exact determinant with the same integer-scaled core used by
1460    /// [`det_exact`](Self::det_exact), then converts the exact scaled integer
1461    /// result to `f64` only if the result is exactly representable as a finite
1462    /// binary64 value. The candidate conversion follows IEEE 754
1463    /// round-to-nearest, ties-to-even, but is returned only when no rounding is
1464    /// required.
1465    ///
1466    /// When callers also need the exact value or may recover with explicit
1467    /// rounding, compute [`det_exact`](Self::det_exact) once and use
1468    /// [`ExactF64Conversion`] on the returned [`BigRational`].
1469    ///
1470    /// # Examples
1471    /// ```
1472    /// use la_stack::prelude::*;
1473    ///
1474    /// # fn main() -> Result<(), LaError> {
1475    /// let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
1476    /// let det = m.det_exact_f64()?;
1477    /// assert!((det - (-2.0)).abs() <= f64::EPSILON);
1478    /// # Ok(())
1479    /// # }
1480    /// ```
1481    ///
1482    /// # Errors
1483    /// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
1484    /// overflows the internal exponent representation.
1485    ///
1486    /// Returns [`LaError::Unrepresentable`] if the exact determinant cannot be
1487    /// represented exactly as a finite `f64`.
1488    #[inline]
1489    pub fn det_exact_f64(&self) -> Result<f64, LaError> {
1490        det_exact_f64_finite(self)
1491    }
1492
1493    /// Exact determinant rounded to `f64`.
1494    ///
1495    /// Requires the `exact` Cargo feature.
1496    ///
1497    /// Computes the exact determinant with the same integer-scaled core used by
1498    /// [`det_exact`](Self::det_exact), then rounds the exact value to a finite
1499    /// binary64 value using IEEE 754 round-to-nearest, ties-to-even. Unlike
1500    /// [`det_exact_f64`](Self::det_exact_f64), this method is intentionally lossy
1501    /// and may round non-dyadic or underflowing nonzero exact determinants.
1502    ///
1503    /// # Examples
1504    /// ```
1505    /// use core::assert_matches;
1506    /// use la_stack::prelude::*;
1507    ///
1508    /// # fn main() -> Result<(), LaError> {
1509    /// let m = Matrix::<2>::try_from_rows([
1510    ///     [1.0 + f64::EPSILON, 0.0],
1511    ///     [0.0, 1.0 - f64::EPSILON],
1512    /// ])?;
1513    ///
1514    /// assert_matches!(
1515    ///     m.det_exact_f64(),
1516    ///     Err(LaError::Unrepresentable {
1517    ///         index: None,
1518    ///         reason: UnrepresentableReason::RequiresRounding,
1519    ///         ..
1520    ///     })
1521    /// );
1522    /// assert_eq!(m.det_exact_rounded_f64()?.to_bits(), 1.0f64.to_bits());
1523    /// # Ok(())
1524    /// # }
1525    /// ```
1526    ///
1527    /// # Errors
1528    /// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
1529    /// overflows the internal exponent representation.
1530    ///
1531    /// Returns [`LaError::Unrepresentable`] if rounding cannot produce a finite `f64`.
1532    #[inline]
1533    pub fn det_exact_rounded_f64(&self) -> Result<f64, LaError> {
1534        det_exact_rounded_f64_finite(self)
1535    }
1536
1537    /// Exact linear system solve using hybrid integer/rational arithmetic.
1538    ///
1539    /// Requires the `exact` Cargo feature.
1540    ///
1541    /// Solves `A x = b` where `A` is `self` and `b` is the given vector.
1542    /// Returns the exact solution as `[BigRational; D]`. Every finite `f64` is
1543    /// exactly representable as a rational, so the conversion is lossless and
1544    /// the result is exact for the stored binary64 entries. It cannot recover
1545    /// precision lost before matrix or vector construction.
1546    ///
1547    /// # When to use
1548    ///
1549    /// Use this when you need a solution exact for the stored inputs — for
1550    /// example, circumcenter computation over stored coordinates for
1551    /// near-degenerate simplices where f64 arithmetic may produce wildly wrong
1552    /// results.
1553    ///
1554    /// # Algorithm
1555    ///
1556    /// Matrix and RHS entries are decomposed via IEEE 754 bit extraction and
1557    /// independently scaled to their own power-of-two bases so both sides of
1558    /// the augmented system `(A | b)` become integer-valued without needless
1559    /// cross-side shifts. After solving that integer system, the exact
1560    /// power-of-two ratio between the RHS and matrix scales is restored.
1561    /// Forward elimination runs entirely in `BigInt`
1562    /// with fraction-free Bareiss updates — no `BigRational`, no GCD, no
1563    /// denominator tracking in the `O(D³)` phase.  Only the upper-triangular
1564    /// result is lifted into `BigRational` for back-substitution (the `O(D²)`
1565    /// phase where fractions are inherent).  First-non-zero pivoting is used
1566    /// throughout; since all arithmetic is exact, any non-zero pivot yields
1567    /// the correct answer (no numerical-stability concerns).
1568    ///
1569    /// # Examples
1570    /// ```
1571    /// use la_stack::prelude::*;
1572    ///
1573    /// # fn main() -> Result<(), LaError> {
1574    /// // A x = b  where A = [[1,2],[3,4]], b = [5, 11]  →  x = [1, 2]
1575    /// let a = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
1576    /// let b = Vector::<2>::try_new([5.0, 11.0])?;
1577    /// let x = a.solve_exact(b)?;
1578    /// assert_eq!(x[0], BigRational::from_integer(1.into()));
1579    /// assert_eq!(x[1], BigRational::from_integer(2.into()));
1580    /// # Ok(())
1581    /// # }
1582    /// ```
1583    ///
1584    /// # Errors
1585    /// Returns [`LaError::Singular`] if the matrix is exactly singular.
1586    #[inline]
1587    pub fn solve_exact(&self, b: Vector<D>) -> Result<[BigRational; D], LaError> {
1588        bareiss_solve_finite(self, &b)
1589    }
1590
1591    /// Exact linear system solve converted to `f64`.
1592    ///
1593    /// Requires the `exact` Cargo feature.
1594    ///
1595    /// Computes the exact [`BigRational`] solution via
1596    /// [`solve_exact`](Self::solve_exact) and converts each component to `f64`
1597    /// only if that component is exactly representable as a finite binary64
1598    /// value. The candidate conversion follows IEEE 754 round-to-nearest,
1599    /// ties-to-even, but is returned only when no rounding is required.
1600    ///
1601    /// When callers also need the exact solution or may recover with explicit
1602    /// rounding, compute [`solve_exact`](Self::solve_exact) once and use
1603    /// [`ExactF64Conversion`] on the returned array.
1604    ///
1605    /// # Examples
1606    /// ```
1607    /// use la_stack::prelude::*;
1608    ///
1609    /// # fn main() -> Result<(), LaError> {
1610    /// let a = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
1611    /// let b = Vector::<2>::try_new([5.0, 11.0])?;
1612    /// let x = a.solve_exact_f64(b)?.into_array();
1613    /// assert!((x[0] - 1.0).abs() <= f64::EPSILON);
1614    /// assert!((x[1] - 2.0).abs() <= f64::EPSILON);
1615    /// # Ok(())
1616    /// # }
1617    /// ```
1618    ///
1619    /// # Errors
1620    /// Returns [`LaError::Singular`] if the matrix is exactly singular.
1621    /// Returns [`LaError::Unrepresentable`] if any component of the exact solution
1622    /// cannot be represented exactly as a finite `f64`.
1623    #[inline]
1624    pub fn solve_exact_f64(&self, b: Vector<D>) -> Result<Vector<D>, LaError> {
1625        self.solve_exact(b)?.try_to_f64()
1626    }
1627
1628    /// Exact linear system solve rounded to `f64`.
1629    ///
1630    /// Requires the `exact` Cargo feature.
1631    ///
1632    /// Computes the exact [`BigRational`] solution via
1633    /// [`solve_exact`](Self::solve_exact) and rounds each component to a finite
1634    /// binary64 value using IEEE 754 round-to-nearest, ties-to-even. Unlike
1635    /// [`solve_exact_f64`](Self::solve_exact_f64), this method is intentionally
1636    /// lossy and may round non-dyadic or underflowing nonzero exact components.
1637    ///
1638    /// # Examples
1639    /// ```
1640    /// use core::assert_matches;
1641    /// use la_stack::prelude::*;
1642    ///
1643    /// # fn main() -> Result<(), LaError> {
1644    /// let a = Matrix::<1>::try_from_rows([[3.0]])?;
1645    /// let b = Vector::<1>::try_new([1.0])?;
1646    ///
1647    /// assert_matches!(
1648    ///     a.solve_exact_f64(b),
1649    ///     Err(LaError::Unrepresentable {
1650    ///         index: Some(0),
1651    ///         reason: UnrepresentableReason::RequiresRounding,
1652    ///         ..
1653    ///     })
1654    /// );
1655    /// assert_eq!(a.solve_exact_rounded_f64(b)?.into_array(), [1.0 / 3.0]);
1656    /// # Ok(())
1657    /// # }
1658    /// ```
1659    ///
1660    /// # Errors
1661    /// Returns [`LaError::Singular`] if the matrix is exactly singular.
1662    /// Returns [`LaError::Unrepresentable`] if rounding any component cannot
1663    /// produce a finite `f64`.
1664    #[inline]
1665    pub fn solve_exact_rounded_f64(&self, b: Vector<D>) -> Result<Vector<D>, LaError> {
1666        self.solve_exact(b)?.to_rounded_f64()
1667    }
1668
1669    /// Exact determinant sign using adaptive-precision arithmetic.
1670    ///
1671    /// Requires the `exact` Cargo feature.
1672    ///
1673    /// Returns [`DeterminantSign::Positive`], [`DeterminantSign::Negative`], or
1674    /// [`DeterminantSign::Zero`] according to the determinant that is exact for
1675    /// the stored binary64 entries. This cannot recover precision lost before
1676    /// matrix construction.
1677    ///
1678    /// For D ≤ 4, a fast f64 filter is tried first: `det_direct()` is compared
1679    /// against a conservative error bound derived from the matrix permanent.
1680    /// If the f64 result clearly exceeds the bound, the sign is returned
1681    /// immediately without allocating. Otherwise, exact integer arithmetic
1682    /// computes the sign without constructing any `BigRational` values: direct
1683    /// `BigInt` expansions for D ≤ 4 and Bareiss elimination for D ≥ 5.
1684    ///
1685    /// # When to use
1686    ///
1687    /// Use this when the sign of the determinant over the stored entries must be
1688    /// correct regardless of floating-point conditioning (e.g. geometric
1689    /// predicates on near-degenerate stored coordinates). For well-conditioned
1690    /// matrices the fast filter resolves the sign without touching
1691    /// `BigRational`, so the overhead is minimal.
1692    ///
1693    /// # Examples
1694    /// ```
1695    /// use la_stack::prelude::*;
1696    ///
1697    /// let m = Matrix::<3>::try_from_rows([
1698    ///     [1.0, 2.0, 3.0],
1699    ///     [4.0, 5.0, 6.0],
1700    ///     [7.0, 8.0, 9.0],
1701    /// ])?;
1702    /// // This matrix is singular (row 3 = row 1 + row 2 in exact arithmetic).
1703    /// assert_eq!(m.det_sign_exact(), DeterminantSign::Zero);
1704    ///
1705    /// assert_eq!(Matrix::<3>::identity().det_sign_exact(), DeterminantSign::Positive);
1706    /// # Ok::<(), LaError>(())
1707    /// ```
1708    #[inline]
1709    pub fn det_sign_exact(&self) -> DeterminantSign {
1710        det_sign_exact_finite(self)
1711    }
1712}
1713
1714#[cfg(test)]
1715mod tests {
1716    use core::assert_matches;
1717    use std::array::from_fn;
1718
1719    use num_traits::Signed;
1720    use pastey::paste;
1721    use proptest::prelude::*;
1722
1723    use super::*;
1724    use crate::{
1725        ArithmeticOperation, DEFAULT_SINGULAR_TOL, NonFiniteLocation, NonFiniteOrigin,
1726        SingularityReason,
1727    };
1728
1729    // -----------------------------------------------------------------------
1730    // Test helpers
1731    // -----------------------------------------------------------------------
1732
1733    /// Build an exact `BigRational` from an `f64` via IEEE 754 bit decomposition.
1734    ///
1735    /// Thin wrapper over [`decompose_f64`] that packs the mantissa/exponent
1736    /// pair into a fully-formed `BigRational` of the form `±m · 2^e`.  The
1737    /// production code paths (`exact_det_int_finite`, `bareiss_solve_finite`) instead
1738    /// decompose entries into scaled `BigInt` collections, which avoids
1739    /// per-entry GCD work in the elimination loops — so this helper
1740    /// is not used by them and lives here to keep test assertions concise
1741    /// (e.g. `assert_eq!(x[0], f64_to_big_rational(3.0))`).
1742    ///
1743    /// See `REFERENCES.md` \[9-10\] for the IEEE 754 standard and Goldberg's
1744    /// survey of floating-point representation.
1745    ///
1746    /// # Panics
1747    /// Panics if `x` is NaN or infinite.
1748    fn f64_to_big_rational(x: f64) -> BigRational {
1749        let component = decompose_f64(x).expect("test helper requires finite f64 input");
1750        let Component::NonZero {
1751            mantissa,
1752            exponent,
1753            is_negative,
1754        } = component
1755        else {
1756            return BigRational::from_integer(BigInt::from(0));
1757        };
1758
1759        let numer = if is_negative {
1760            -BigInt::from(mantissa.get())
1761        } else {
1762            BigInt::from(mantissa.get())
1763        };
1764
1765        if exponent >= 0 {
1766            BigRational::new_raw(numer << exponent.cast_unsigned(), BigInt::from(1u32))
1767        } else {
1768            BigRational::new_raw(numer, BigInt::from(1u32) << (-exponent).cast_unsigned())
1769        }
1770    }
1771
1772    fn assert_non_finite_input_scalar<T>(result: &Result<T, LaError>) {
1773        let Err(error) = result else {
1774            panic!("expected a non-finite scalar-input error");
1775        };
1776        assert!(matches!(
1777            *error,
1778            LaError::NonFinite {
1779                location: NonFiniteLocation::Scalar,
1780                origin: NonFiniteOrigin::Input,
1781                ..
1782            }
1783        ));
1784    }
1785
1786    fn assert_unrepresentable<T>(
1787        result: &Result<T, LaError>,
1788        expected_index: Option<usize>,
1789        expected_reason: UnrepresentableReason,
1790    ) {
1791        let Err(error) = result else {
1792            panic!("expected an exact-to-f64 conversion error");
1793        };
1794        assert!(matches!(
1795            *error,
1796            LaError::Unrepresentable { index, reason, .. }
1797                if index == expected_index && reason == expected_reason
1798        ));
1799    }
1800
1801    // -----------------------------------------------------------------------
1802    // Macro-generated per-dimension tests (D=2..5)
1803    // -----------------------------------------------------------------------
1804
1805    macro_rules! gen_exact_identity_tests {
1806        ($d:literal) => {
1807            paste! {
1808                #[test]
1809                fn [<exact_identity_paths_ $d d>]() {
1810                    let matrix = Matrix::<$d>::identity();
1811                    let one = BigRational::from_integer(BigInt::from(1));
1812
1813                    assert_eq!(matrix.det_exact().unwrap(), one);
1814                    assert_eq!(matrix.det_exact_f64().unwrap().to_bits(), 1.0_f64.to_bits());
1815                    assert_eq!(
1816                        matrix.det_exact_rounded_f64().unwrap().to_bits(),
1817                        1.0_f64.to_bits()
1818                    );
1819                    assert_eq!(matrix.det_sign_exact(), DeterminantSign::Positive);
1820                }
1821            }
1822        };
1823    }
1824
1825    gen_exact_identity_tests!(2);
1826    gen_exact_identity_tests!(3);
1827    gen_exact_identity_tests!(4);
1828    gen_exact_identity_tests!(5);
1829
1830    /// For D ≤ 4, `det_exact_f64` should agree with `det_direct` on matrices
1831    /// whose exact determinant is representable in f64.
1832    macro_rules! gen_det_exact_f64_agrees_with_det_direct {
1833        ($d:literal) => {
1834            paste! {
1835                #[test]
1836                fn [<det_exact_f64_agrees_with_det_direct_ $d d>]() {
1837                    // Power-of-two diagonal entries make the determinant
1838                    // exactly representable in binary64.
1839                    let mut rows = [[0.0f64; $d]; $d];
1840                    let mut value = 2.0;
1841                    for (i, row) in rows.iter_mut().enumerate() {
1842                        row[i] = value;
1843                        value *= 2.0;
1844                    }
1845                    let m = Matrix::<$d>::try_from_rows(rows).unwrap();
1846                    let exact = m.det_exact_f64().unwrap();
1847                    let direct = m.det_direct().unwrap().unwrap();
1848                    assert_eq!(exact.to_bits(), direct.to_bits());
1849                }
1850            }
1851        };
1852    }
1853
1854    gen_det_exact_f64_agrees_with_det_direct!(2);
1855    gen_det_exact_f64_agrees_with_det_direct!(3);
1856    gen_det_exact_f64_agrees_with_det_direct!(4);
1857
1858    #[test]
1859    fn det_sign_exact_d0_is_positive() {
1860        assert_eq!(
1861            Matrix::<0>::zero().det_sign_exact(),
1862            DeterminantSign::Positive
1863        );
1864    }
1865
1866    #[test]
1867    fn det_sign_exact_d1_positive() {
1868        let m = Matrix::<1>::try_from_rows([[42.0]]).unwrap();
1869        assert_eq!(m.det_sign_exact(), DeterminantSign::Positive);
1870    }
1871
1872    #[test]
1873    fn det_sign_exact_d1_negative() {
1874        let m = Matrix::<1>::try_from_rows([[-3.5]]).unwrap();
1875        assert_eq!(m.det_sign_exact(), DeterminantSign::Negative);
1876    }
1877
1878    #[test]
1879    fn det_sign_exact_d1_zero() {
1880        let m = Matrix::<1>::try_from_rows([[0.0]]).unwrap();
1881        assert_eq!(m.det_sign_exact(), DeterminantSign::Zero);
1882    }
1883
1884    #[test]
1885    fn det_sign_exact_singular_duplicate_rows() {
1886        let m = Matrix::<3>::try_from_rows([
1887            [1.0, 2.0, 3.0],
1888            [4.0, 5.0, 6.0],
1889            [1.0, 2.0, 3.0], // duplicate of row 0
1890        ])
1891        .unwrap();
1892        assert_eq!(m.det_sign_exact(), DeterminantSign::Zero);
1893    }
1894
1895    #[test]
1896    fn det_sign_exact_singular_linear_combination() {
1897        // Row 2 = row 0 + row 1 in exact arithmetic.
1898        let m = Matrix::<3>::try_from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [5.0, 7.0, 9.0]])
1899            .unwrap();
1900        assert_eq!(m.det_sign_exact(), DeterminantSign::Zero);
1901    }
1902
1903    #[test]
1904    fn det_sign_exact_negative_det_row_swap() {
1905        // Swapping two rows of the identity negates the determinant.
1906        let m = Matrix::<3>::try_from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
1907            .unwrap();
1908        assert_eq!(m.det_sign_exact(), DeterminantSign::Negative);
1909    }
1910
1911    #[test]
1912    fn det_sign_exact_negative_det_known() {
1913        // det([[1,2],[3,4]]) = 1*4 - 2*3 = -2
1914        let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
1915        assert_eq!(m.det_sign_exact(), DeterminantSign::Negative);
1916    }
1917
1918    #[test]
1919    fn det_sign_exact_agrees_with_det_for_spd() {
1920        // SPD matrix → positive determinant.
1921        let m = Matrix::<3>::try_from_rows([[4.0, 2.0, 0.0], [2.0, 5.0, 1.0], [0.0, 1.0, 3.0]])
1922            .unwrap();
1923        assert_eq!(m.det_sign_exact(), DeterminantSign::Positive);
1924        assert!(m.det().unwrap() > 0.0);
1925    }
1926
1927    /// Near-singular matrix with an exact perturbation.
1928    ///
1929    /// The base matrix `[[1,2,3],[4,5,6],[7,8,9]]` is exactly singular (rows in
1930    /// arithmetic progression).  Adding `2^-50` to entry (0,0) makes
1931    /// `det = 2^-50 × cofactor(0,0) = 2^-50 × (5×9 − 6×8) = −3 × 2^-50 < 0`.
1932    /// Both f64 `det_direct()` and `det_sign_exact()` should agree here.
1933    #[test]
1934    fn det_sign_exact_near_singular_perturbation() {
1935        let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); // 2^-50
1936        let m = Matrix::<3>::try_from_rows([
1937            [1.0 + perturbation, 2.0, 3.0],
1938            [4.0, 5.0, 6.0],
1939            [7.0, 8.0, 9.0],
1940        ])
1941        .unwrap();
1942        // Exact: det = perturbation × (5×9 − 6×8) = perturbation × (−3) < 0.
1943        assert_eq!(m.det_sign_exact(), DeterminantSign::Negative);
1944    }
1945
1946    /// For D ≤ 4, well-conditioned matrices should hit the fast filter
1947    /// and never allocate `BigRational`.  We can't directly observe this,
1948    /// but we verify correctness for a range of known signs.
1949    #[test]
1950    fn det_sign_exact_fast_filter_positive_4x4() {
1951        let m = Matrix::<4>::try_from_rows([
1952            [2.0, 1.0, 0.0, 0.0],
1953            [1.0, 3.0, 1.0, 0.0],
1954            [0.0, 1.0, 4.0, 1.0],
1955            [0.0, 0.0, 1.0, 5.0],
1956        ])
1957        .unwrap();
1958        // SPD tridiagonal → positive det.
1959        assert_eq!(m.det_sign_exact(), DeterminantSign::Positive);
1960    }
1961
1962    #[test]
1963    fn det_sign_exact_fast_filter_negative_4x4() {
1964        // Swap rows 0 and 1 of the above → negate det.
1965        let m = Matrix::<4>::try_from_rows([
1966            [1.0, 3.0, 1.0, 0.0],
1967            [2.0, 1.0, 0.0, 0.0],
1968            [0.0, 1.0, 4.0, 1.0],
1969            [0.0, 0.0, 1.0, 5.0],
1970        ])
1971        .unwrap();
1972        assert_eq!(m.det_sign_exact(), DeterminantSign::Negative);
1973    }
1974
1975    #[test]
1976    fn det_sign_exact_subnormal_entries() {
1977        // Subnormal f64 values should convert losslessly.
1978        let tiny = 5e-324_f64; // smallest positive subnormal
1979        assert!(tiny.is_subnormal());
1980
1981        let m = Matrix::<2>::try_from_rows([[tiny, 0.0], [0.0, tiny]]).unwrap();
1982        // det = tiny^2 > 0
1983        assert_eq!(m.det_sign_exact(), DeterminantSign::Positive);
1984    }
1985
1986    #[test]
1987    fn det_sign_exact_falls_back_when_subnormal_rounding_reverses_direct_sign() {
1988        let scale = 2.0_f64.powi(-360);
1989        let matrix = Matrix::<3>::try_from_rows([
1990            [-5.0 * scale, 3.0 * scale, 6.0 * scale],
1991            [0.0, -7.0 * scale, -7.0 * scale],
1992            [2.0 * scale, -3.0 * scale, -4.0 * scale],
1993        ])
1994        .unwrap();
1995
1996        assert_eq!(
1997            matrix.det_direct().unwrap().unwrap().to_bits(),
1998            (-f64::from_bits(1)).to_bits()
1999        );
2000        assert_eq!(matrix.det_errbound(), Ok(None));
2001        assert!(matrix.det_exact().unwrap().is_positive());
2002        assert_eq!(matrix.det_sign_exact(), DeterminantSign::Positive);
2003    }
2004
2005    #[test]
2006    fn det_sign_exact_falls_back_for_bit_exact_underflow_counterexample() {
2007        let matrix = Matrix::<3>::try_from_rows([
2008            [
2009                f64::from_bits(9_218_868_437_227_405_311),
2010                f64::from_bits(13_830_554_455_654_793_216),
2011                0.0,
2012            ],
2013            [
2014                f64::from_bits(6_790_500_848_393_242_208),
2015                f64::from_bits(2_184_621_143_747_520_227),
2016                f64::from_bits(2_187_555_472_467_513_745),
2017            ],
2018            [
2019                0.0,
2020                f64::from_bits(2_184_859_204_554_904_434),
2021                f64::from_bits(2_184_762_736_385_916_910),
2022            ],
2023        ])
2024        .unwrap();
2025
2026        assert!(matrix.det_direct().unwrap().unwrap().is_sign_positive());
2027        assert_eq!(matrix.det_errbound(), Ok(None));
2028        assert!(matrix.det_exact().unwrap().is_negative());
2029        assert_eq!(matrix.det_sign_exact(), DeterminantSign::Negative);
2030    }
2031
2032    #[test]
2033    fn det_sign_exact_pivot_needed_5x5() {
2034        // D ≥ 5 skips the fast filter → exercises Bareiss pivoting.
2035        // Permutation matrix with a single swap (rows 0↔1) → det = −1.
2036        let m = Matrix::<5>::try_from_rows([
2037            [0.0, 1.0, 0.0, 0.0, 0.0],
2038            [1.0, 0.0, 0.0, 0.0, 0.0],
2039            [0.0, 0.0, 1.0, 0.0, 0.0],
2040            [0.0, 0.0, 0.0, 1.0, 0.0],
2041            [0.0, 0.0, 0.0, 0.0, 1.0],
2042        ])
2043        .unwrap();
2044        assert_eq!(m.det_sign_exact(), DeterminantSign::Negative);
2045    }
2046
2047    #[test]
2048    fn det_sign_exact_5x5_known() {
2049        // det of a permutation matrix with two swaps = +1 (even permutation).
2050        let m = Matrix::<5>::try_from_rows([
2051            [0.0, 1.0, 0.0, 0.0, 0.0],
2052            [1.0, 0.0, 0.0, 0.0, 0.0],
2053            [0.0, 0.0, 0.0, 1.0, 0.0],
2054            [0.0, 0.0, 1.0, 0.0, 0.0],
2055            [0.0, 0.0, 0.0, 0.0, 1.0],
2056        ])
2057        .unwrap();
2058        // Two transpositions → even permutation → det = +1
2059        assert_eq!(m.det_sign_exact(), DeterminantSign::Positive);
2060    }
2061
2062    // -----------------------------------------------------------------------
2063    // Direct tests for internal helpers (coverage of private functions)
2064    // -----------------------------------------------------------------------
2065
2066    #[test]
2067    fn det_errbound_d0_is_zero() {
2068        assert_eq!(Matrix::<0>::zero().det_errbound(), Ok(Some(0.0)));
2069    }
2070
2071    #[test]
2072    fn det_errbound_d1_is_zero() {
2073        assert_eq!(
2074            Matrix::<1>::try_from_rows([[42.0]]).unwrap().det_errbound(),
2075            Ok(Some(0.0))
2076        );
2077    }
2078
2079    #[test]
2080    fn det_errbound_d3_non_identity() {
2081        // Non-identity matrix to exercise all code paths in D=3 case
2082        let m = Matrix::<3>::try_from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 10.0]])
2083            .unwrap();
2084        let bound = m.det_errbound().unwrap().unwrap();
2085        assert!(bound > 0.0);
2086    }
2087
2088    #[test]
2089    fn det_errbound_d4_non_identity() {
2090        // Non-identity matrix to exercise all code paths in D=4 case
2091        let m = Matrix::<4>::try_from_rows([
2092            [1.0, 0.0, 0.0, 0.0],
2093            [0.0, 2.0, 0.0, 0.0],
2094            [0.0, 0.0, 3.0, 0.0],
2095            [0.0, 0.0, 0.0, 4.0],
2096        ])
2097        .unwrap();
2098        let bound = m.det_errbound().unwrap().unwrap();
2099        assert!(bound > 0.0);
2100    }
2101
2102    // -----------------------------------------------------------------------
2103    // decompose_f64 tests
2104    // -----------------------------------------------------------------------
2105
2106    #[test]
2107    fn decompose_f64_zero() {
2108        assert_eq!(decompose_f64(0.0), Ok(Component::Zero));
2109        assert_eq!(decompose_f64(-0.0), Ok(Component::Zero));
2110    }
2111
2112    #[test]
2113    fn decompose_f64_one() {
2114        assert_eq!(
2115            decompose_f64(1.0),
2116            Ok(Component::NonZero {
2117                mantissa: NonZeroU64::new(1).unwrap(),
2118                exponent: 0,
2119                is_negative: false,
2120            })
2121        );
2122    }
2123
2124    #[test]
2125    fn decompose_f64_negative() {
2126        assert_eq!(
2127            decompose_f64(-3.5),
2128            Ok(Component::NonZero {
2129                mantissa: NonZeroU64::new(7).unwrap(),
2130                exponent: -1,
2131                is_negative: true,
2132            })
2133        );
2134    }
2135
2136    #[test]
2137    fn decompose_f64_subnormal() {
2138        let tiny = f64::from_bits(1);
2139        assert!(tiny.is_subnormal());
2140        assert_eq!(
2141            decompose_f64(tiny),
2142            Ok(Component::NonZero {
2143                mantissa: NonZeroU64::new(1).unwrap(),
2144                exponent: -1074,
2145                is_negative: false,
2146            })
2147        );
2148    }
2149
2150    #[test]
2151    fn decompose_f64_normalizes_mixed_subnormal_mantissa() {
2152        let value = f64::from_bits(0x000C_0000_0000_0000);
2153        assert!(value.is_subnormal());
2154        assert_eq!(
2155            decompose_f64(value),
2156            Ok(Component::NonZero {
2157                mantissa: NonZeroU64::new(3).unwrap(),
2158                exponent: -1024,
2159                is_negative: false,
2160            })
2161        );
2162    }
2163
2164    #[test]
2165    fn decompose_f64_power_of_two() {
2166        assert_eq!(
2167            decompose_f64(1024.0),
2168            Ok(Component::NonZero {
2169                mantissa: NonZeroU64::new(1).unwrap(),
2170                exponent: 10,
2171                is_negative: false,
2172            })
2173        );
2174    }
2175
2176    #[test]
2177    fn decompose_f64_rejects_nan() {
2178        assert_non_finite_input_scalar(&decompose_f64(f64::NAN));
2179    }
2180
2181    proptest! {
2182        #[test]
2183        fn finite_f64_round_trips_through_exact_decomposition(bits in any::<u64>()) {
2184            let value = f64::from_bits(bits);
2185            prop_assume!(value.is_finite());
2186
2187            if let Ok(Component::NonZero { mantissa, .. }) = decompose_f64(value) {
2188                prop_assert_eq!(mantissa.get() & 1, 1);
2189            }
2190
2191            let exact = f64_to_big_rational(value);
2192            let reconstructed = exact_rational_to_finite_f64(&exact, None);
2193
2194            prop_assert_eq!(reconstructed, Ok(value));
2195        }
2196    }
2197
2198    #[test]
2199    fn wide_low_exponent_value_reports_non_finite_rounded_result() {
2200        // (2^2099 - 1) × 2^-1075 lies just below 2^1024 and rounds to +∞.
2201        let value = (BigInt::from(1_u8) << 2099_u32) - BigInt::from(1_u8);
2202        let result = big_int_exp_to_finite_f64(&value, -1075, None);
2203
2204        assert!(!result.as_ref().unwrap_err().requires_rounding());
2205        assert_unrepresentable(&result, None, UnrepresentableReason::NotFinite);
2206    }
2207
2208    #[test]
2209    fn direct_big_int_rounding_handles_extreme_negative_exponent_without_large_denominator() {
2210        let positive = big_int_exp_ref_to_rounded_f64(&BigInt::from(1_u8), i32::MIN, None).unwrap();
2211        let negative =
2212            big_int_exp_ref_to_rounded_f64(&BigInt::from(-1_i8), i32::MIN, None).unwrap();
2213
2214        assert_eq!(positive.to_bits(), 0.0_f64.to_bits());
2215        assert_eq!(negative.to_bits(), (-0.0_f64).to_bits());
2216    }
2217
2218    proptest! {
2219        #[test]
2220        fn direct_big_int_rounding_matches_rational_oracle(
2221            value in any::<i128>(),
2222            exp in -1200_i32..=1200_i32,
2223        ) {
2224            let value = BigInt::from(value);
2225            let direct = big_int_exp_ref_to_rounded_f64(&value, exp, None);
2226            let exact = big_int_exp_to_big_rational(value, exp);
2227            let oracle = exact_rational_to_rounded_f64(&exact, None);
2228
2229            match (direct, oracle) {
2230                (Ok(actual), Ok(expected)) => {
2231                    prop_assert_eq!(actual.to_bits(), expected.to_bits());
2232                }
2233                (Err(actual), Err(expected)) => {
2234                    prop_assert_eq!(actual, expected);
2235                }
2236                (actual, expected) => {
2237                    prop_assert_eq!(actual, expected);
2238                }
2239            }
2240        }
2241    }
2242
2243    #[test]
2244    fn component_to_big_int_distinguishes_zero_from_nonzero_mantissa() {
2245        let baseline = Component::NonZero {
2246            mantissa: NonZeroU64::new(1).unwrap(),
2247            exponent: 1,
2248            is_negative: false,
2249        };
2250        let positive = Component::NonZero {
2251            mantissa: NonZeroU64::new(3).unwrap(),
2252            exponent: 4,
2253            is_negative: false,
2254        };
2255        let negative = Component::NonZero {
2256            mantissa: NonZeroU64::new(5).unwrap(),
2257            exponent: 3,
2258            is_negative: true,
2259        };
2260
2261        let decomposed =
2262            Decomposed::from_vector_components([Component::Zero, baseline, positive, negative]);
2263        let scale = ScaleExponent::for_decomposed(&decomposed);
2264
2265        assert_eq!(
2266            component_to_big_int(Component::Zero, scale),
2267            BigInt::from(0)
2268        );
2269        assert_eq!(component_to_big_int(positive, scale), BigInt::from(24));
2270        assert_eq!(component_to_big_int(negative, scale), BigInt::from(-20));
2271    }
2272
2273    #[test]
2274    fn decomposed_all_zero_uses_no_sentinel_exponent() {
2275        let decomposed = decompose_proven_finite_matrix(&Matrix::<2>::zero());
2276        assert_eq!(decomposed.min_exponent(), None);
2277
2278        let scale = ScaleExponent::for_decomposed(&decomposed);
2279        assert_eq!(scale, ScaleExponent::ZERO);
2280        assert_eq!(scale.get(), 0);
2281        assert_eq!(
2282            build_big_int_matrix(decomposed.components(), scale),
2283            [
2284                [BigInt::from(0), BigInt::from(0)],
2285                [BigInt::from(0), BigInt::from(0)]
2286            ]
2287        );
2288    }
2289
2290    #[test]
2291    fn matrix_and_rhs_scales_are_derived_independently() {
2292        let tiny = f64::from_bits(1);
2293        let matrix = Matrix::<2>::try_from_rows([[f64::MAX, 0.0], [0.0, 1.0]]).unwrap();
2294        let rhs = Vector::<2>::try_new([tiny, 0.0]).unwrap();
2295        let matrix = decompose_proven_finite_matrix(&matrix);
2296        let rhs = decompose_proven_finite_vector(&rhs);
2297
2298        assert_eq!(matrix.min_exponent(), Some(0));
2299        assert_eq!(rhs.min_exponent(), Some(-1074));
2300
2301        let matrix_scale = ScaleExponent::for_decomposed(&matrix);
2302        let rhs_scale = ScaleExponent::for_decomposed(&rhs);
2303        assert_eq!(matrix_scale.get(), 0);
2304        assert_eq!(matrix_scale.shift_for(0), 0);
2305        assert_eq!(rhs_scale.get(), -1074);
2306        assert_eq!(rhs_scale.shift_for(-1074), 0);
2307    }
2308
2309    proptest! {
2310        #[test]
2311        fn derived_scale_yields_nonnegative_shifts(bits in any::<[u64; 4]>()) {
2312            let values = bits.map(f64::from_bits);
2313            prop_assume!(values.iter().all(|value| value.is_finite()));
2314            let matrix = Matrix::<2>::try_from_rows([
2315                [values[0], values[1]],
2316                [values[2], values[3]],
2317            ]).unwrap();
2318            let decomposed = decompose_proven_finite_matrix(&matrix);
2319            let scale = ScaleExponent::for_decomposed(&decomposed);
2320
2321            for component in decomposed.components().iter().flatten() {
2322                if let Some(exponent) = component.exponent() {
2323                    prop_assert!(exponent >= scale.get());
2324                    prop_assert_eq!(
2325                        scale.shift_for(exponent),
2326                        u32::try_from(exponent - scale.get()).unwrap(),
2327                    );
2328                }
2329            }
2330        }
2331    }
2332
2333    #[test]
2334    fn determinant_scale_exp_multiplies_dimension_and_min_exponent() {
2335        assert_eq!(determinant_scale_exp::<4>(-1074), Ok(-4296));
2336    }
2337
2338    #[test]
2339    fn determinant_scale_exp_rejects_dimension_too_large_for_i32() {
2340        assert_eq!(
2341            determinant_scale_exp::<{ i32::MAX as usize + 1 }>(-1074),
2342            Err(LaError::DeterminantScaleOverflow {
2343                dim: i32::MAX as usize + 1,
2344                min_exponent: -1074,
2345            })
2346        );
2347    }
2348
2349    #[test]
2350    fn determinant_scale_exp_rejects_exponent_product_overflow() {
2351        assert_eq!(
2352            determinant_scale_exp::<3_000_000>(-1074),
2353            Err(LaError::DeterminantScaleOverflow {
2354                dim: 3_000_000,
2355                min_exponent: -1074,
2356            })
2357        );
2358    }
2359
2360    #[test]
2361    fn negative_exponent_from_magnitude_covers_i32_domain_boundaries() {
2362        assert_eq!(negative_exponent_from_magnitude(0), 0);
2363        assert_eq!(negative_exponent_from_magnitude(1), -1);
2364        assert_eq!(
2365            negative_exponent_from_magnitude(i32::MAX.cast_unsigned().into()),
2366            -i32::MAX
2367        );
2368        assert_eq!(
2369            negative_exponent_from_magnitude(i32::MIN.unsigned_abs().into()),
2370            i32::MIN
2371        );
2372    }
2373
2374    #[test]
2375    #[should_panic(expected = "negative exponent magnitude exceeds the i32 domain")]
2376    fn negative_exponent_from_magnitude_rejects_values_above_i32_domain() {
2377        let _ = negative_exponent_from_magnitude(u64::from(i32::MIN.unsigned_abs()) + 1);
2378    }
2379
2380    // -----------------------------------------------------------------------
2381    // Exact scaled-integer determinant tests
2382    // -----------------------------------------------------------------------
2383
2384    #[test]
2385    fn exact_det_int_d0() {
2386        let m = Matrix::<0>::zero();
2387        let (det, exp) = exact_det_int_finite(&m).unwrap();
2388        assert_eq!(det, BigInt::from(1));
2389        assert_eq!(exp, 0);
2390    }
2391
2392    /// Table-driven coverage of the D=1 fast-path: each 1×1 matrix
2393    /// decomposes to `(±mant, exp)` directly.  Includes an integer, zero,
2394    /// a negative fractional, and a positive fractional case — the
2395    /// combinations that exercise the sign handling, the all-zero early
2396    /// return, trailing-zero stripping, and negative exponent scaling.
2397    #[test]
2398    fn exact_det_int_d1_cases() {
2399        let cases: &[(f64, i64, i32)] = &[
2400            // (input, expected_det_int, expected_exp)
2401            (7.0, 7, 0),    // integer → (7, 0)
2402            (0.0, 0, 0),    // all-zero early return → (0, 0)
2403            (-3.5, -7, -1), // -3.5 = -7 × 2^(-1)
2404            (0.5, 1, -1),   // 0.5  =  1 × 2^(-1)
2405        ];
2406        for &(input, expected_det_int, expected_exp) in cases {
2407            let m = Matrix::<1>::try_from_rows([[input]]).unwrap();
2408            let (det, exp) = exact_det_int_finite(&m).unwrap();
2409            assert_eq!(
2410                det,
2411                BigInt::from(expected_det_int),
2412                "det_int for input={input}"
2413            );
2414            assert_eq!(exp, expected_exp, "exp for input={input}");
2415        }
2416    }
2417
2418    #[test]
2419    fn exact_det_int_d2_known() {
2420        // det([[1,2],[3,4]]) = -2
2421        let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
2422        let (det_int, total_exp) = exact_det_int_finite(&m).unwrap();
2423        // Reconstruct and verify.
2424        let det = big_int_exp_to_big_rational(det_int, total_exp);
2425        assert_eq!(det, BigRational::from_integer(BigInt::from(-2)));
2426    }
2427
2428    #[test]
2429    fn exact_det_int_all_zeros() {
2430        let m = Matrix::<3>::zero();
2431        let (det, _) = exact_det_int_finite(&m).unwrap();
2432        assert_eq!(det, BigInt::from(0));
2433    }
2434
2435    #[test]
2436    fn exact_det_int_fractional_entries() {
2437        // Entries with negative exponents: 0.5 = 1×2^(-1), 0.25 = 1×2^(-2).
2438        // det([[0.5, 0.25], [1.0, 1.0]]) = 0.5×1.0 − 0.25×1.0 = 0.25
2439        let m = Matrix::<2>::try_from_rows([[0.5, 0.25], [1.0, 1.0]]).unwrap();
2440        let (det_int, total_exp) = exact_det_int_finite(&m).unwrap();
2441        let det = big_int_exp_to_big_rational(det_int, total_exp);
2442        assert_eq!(det, BigRational::new(BigInt::from(1), BigInt::from(4)));
2443    }
2444
2445    #[test]
2446    fn exact_det_int_d3_direct_expansion_handles_zero_diagonal() {
2447        // A zero diagonal entry does not require pivoting in the direct D=3 expansion.
2448        let m = Matrix::<3>::try_from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
2449            .unwrap();
2450        let (det_int, total_exp) = exact_det_int_finite(&m).unwrap();
2451        let det = big_int_exp_to_big_rational(det_int, total_exp);
2452        assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
2453    }
2454
2455    // -----------------------------------------------------------------------
2456    // big_int_exp_to_big_rational tests
2457    // -----------------------------------------------------------------------
2458
2459    #[test]
2460    fn big_int_exp_to_big_rational_zero() {
2461        let r = big_int_exp_to_big_rational(BigInt::from(0), -50);
2462        assert_eq!(r, BigRational::from_integer(BigInt::from(0)));
2463    }
2464
2465    #[test]
2466    fn big_int_exp_to_big_rational_positive_exp() {
2467        // 3 × 2^2 = 12
2468        let r = big_int_exp_to_big_rational(BigInt::from(3), 2);
2469        assert_eq!(r, BigRational::from_integer(BigInt::from(12)));
2470    }
2471
2472    #[test]
2473    fn big_int_exp_to_big_rational_negative_exp_reduced() {
2474        // 6 × 2^(-2) = 6/4 → reduced to 3/2 (strip one shared factor of 2)
2475        let r = big_int_exp_to_big_rational(BigInt::from(6), -2);
2476        assert_eq!(*r.numer(), BigInt::from(3));
2477        assert_eq!(*r.denom(), BigInt::from(2));
2478    }
2479
2480    #[test]
2481    fn big_int_exp_to_big_rational_negative_exp_reduces_to_integer() {
2482        // 8 × 2^(-3) = 1 after stripping every denominator factor.
2483        let r = big_int_exp_to_big_rational(BigInt::from(8), -3);
2484        assert_eq!(r, BigRational::from_integer(BigInt::from(1)));
2485    }
2486
2487    #[test]
2488    fn big_int_exp_to_big_rational_negative_exp_already_odd() {
2489        // 3 × 2^(-2) = 3/4 (already in lowest terms since 3 is odd)
2490        let r = big_int_exp_to_big_rational(BigInt::from(3), -2);
2491        assert_eq!(*r.numer(), BigInt::from(3));
2492        assert_eq!(*r.denom(), BigInt::from(4));
2493    }
2494
2495    #[test]
2496    fn big_int_exp_to_big_rational_negative_value() {
2497        // -5 × 2^1 = -10
2498        let r = big_int_exp_to_big_rational(BigInt::from(-5), 1);
2499        assert_eq!(r, BigRational::from_integer(BigInt::from(-10)));
2500    }
2501
2502    #[test]
2503    fn big_int_exp_to_big_rational_negative_value_with_denominator() {
2504        // -3 × 2^(-2) = -3/4
2505        let r = big_int_exp_to_big_rational(BigInt::from(-3), -2);
2506        assert_eq!(*r.numer(), BigInt::from(-3));
2507        assert_eq!(*r.denom(), BigInt::from(4));
2508    }
2509
2510    // -----------------------------------------------------------------------
2511    // Public exact determinant wrapper tests
2512    // -----------------------------------------------------------------------
2513
2514    #[test]
2515    fn det_exact_d1_returns_entry() {
2516        let det = Matrix::<1>::try_from_rows([[7.0]])
2517            .unwrap()
2518            .det_exact()
2519            .unwrap();
2520        assert_eq!(det, f64_to_big_rational(7.0));
2521    }
2522
2523    #[test]
2524    fn det_exact_d3_direct_expansion_handles_zero_diagonal() {
2525        // Direct D=3 expansion handles a zero diagonal entry without pivoting.
2526        let m = Matrix::<3>::try_from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
2527            .unwrap();
2528        let det = m.det_exact().unwrap();
2529        // det of this permutation matrix = -1
2530        assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
2531    }
2532
2533    #[test]
2534    fn det_exact_d3_singular_zero_column_returns_zero() {
2535        // A zero column makes the direct D=3 determinant exactly zero.
2536        let m = Matrix::<3>::try_from_rows([[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
2537            .unwrap();
2538        let det = m.det_exact().unwrap();
2539        assert_eq!(det, BigRational::from_integer(BigInt::from(0)));
2540    }
2541
2542    #[test]
2543    fn det_sign_exact_overflow_determinant_finite_entries() {
2544        // Entries near f64::MAX are finite, but the f64 determinant overflows
2545        // to infinity. The fast filter is inconclusive and the direct `BigInt`
2546        // expansion computes the correct positive sign.
2547        let big = f64::MAX / 2.0;
2548        assert!(big.is_finite());
2549        let m = Matrix::<3>::try_from_rows([[0.0, 0.0, 1.0], [big, 0.0, 1.0], [0.0, big, 1.0]])
2550            .unwrap();
2551        // det = big^2 > 0
2552        assert_eq!(m.det_sign_exact(), DeterminantSign::Positive);
2553    }
2554
2555    // -----------------------------------------------------------------------
2556    // det_exact: dimension-specific tests
2557    // -----------------------------------------------------------------------
2558
2559    #[test]
2560    fn det_exact_d0_is_one() {
2561        let det = Matrix::<0>::zero().det_exact().unwrap();
2562        assert_eq!(det, BigRational::from_integer(BigInt::from(1)));
2563    }
2564
2565    #[test]
2566    fn det_exact_known_2x2() {
2567        // det([[1,2],[3,4]]) = 1*4 - 2*3 = -2
2568        let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
2569        let det = m.det_exact().unwrap();
2570        assert_eq!(det, BigRational::from_integer(BigInt::from(-2)));
2571    }
2572
2573    #[test]
2574    fn det_exact_known_dense_4x4() {
2575        let m = Matrix::<4>::try_from_rows([
2576            [4.0, 1.0, 3.0, 2.0],
2577            [0.0, 5.0, 2.0, 1.0],
2578            [7.0, 2.0, 6.0, 3.0],
2579            [1.0, 8.0, 4.0, 9.0],
2580        ])
2581        .unwrap();
2582
2583        assert_eq!(
2584            m.det_exact(),
2585            Ok(BigRational::from_integer(BigInt::from(92)))
2586        );
2587    }
2588
2589    #[test]
2590    fn det_exact_singular_returns_zero() {
2591        // Rows in arithmetic progression → exactly singular.
2592        let m = Matrix::<3>::try_from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]])
2593            .unwrap();
2594        let det = m.det_exact().unwrap();
2595        assert_eq!(det, BigRational::from_integer(BigInt::from(0)));
2596    }
2597
2598    #[test]
2599    fn det_exact_near_singular_perturbation() {
2600        // Same 2^-50 perturbation case: exact det = -3 × 2^-50.
2601        let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); // 2^-50
2602        let m = Matrix::<3>::try_from_rows([
2603            [1.0 + perturbation, 2.0, 3.0],
2604            [4.0, 5.0, 6.0],
2605            [7.0, 8.0, 9.0],
2606        ])
2607        .unwrap();
2608        let det = m.det_exact().unwrap();
2609        // det should be exactly -3 × 2^-50.
2610        let expected = BigRational::new(BigInt::from(-3), BigInt::from(1u64 << 50));
2611        assert_eq!(det, expected);
2612    }
2613
2614    #[test]
2615    fn det_exact_5x5_permutation() {
2616        // Single swap (rows 0↔1) → det = -1.
2617        let m = Matrix::<5>::try_from_rows([
2618            [0.0, 1.0, 0.0, 0.0, 0.0],
2619            [1.0, 0.0, 0.0, 0.0, 0.0],
2620            [0.0, 0.0, 1.0, 0.0, 0.0],
2621            [0.0, 0.0, 0.0, 1.0, 0.0],
2622            [0.0, 0.0, 0.0, 0.0, 1.0],
2623        ])
2624        .unwrap();
2625        let det = m.det_exact().unwrap();
2626        assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
2627    }
2628
2629    // -----------------------------------------------------------------------
2630    // det_exact_f64: dimension-specific tests
2631    // -----------------------------------------------------------------------
2632
2633    #[test]
2634    fn det_exact_f64_known_2x2() {
2635        let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
2636        let det = m.det_exact_f64().unwrap();
2637        assert!((det - (-2.0)).abs() <= f64::EPSILON);
2638    }
2639
2640    #[test]
2641    fn det_exact_f64_overflow_returns_err() {
2642        // Entries near f64::MAX produce a determinant too large for f64.
2643        let big = f64::MAX / 2.0;
2644        let m = Matrix::<3>::try_from_rows([[0.0, 0.0, 1.0], [big, 0.0, 1.0], [0.0, big, 1.0]])
2645            .unwrap();
2646        // det = big^2, which overflows f64.
2647        assert_unrepresentable(&m.det_exact_f64(), None, UnrepresentableReason::NotFinite);
2648    }
2649
2650    #[test]
2651    fn det_exact_rounded_f64_overflow_returns_err() {
2652        let big = f64::MAX / 2.0;
2653        let m = Matrix::<3>::try_from_rows([[0.0, 0.0, 1.0], [big, 0.0, 1.0], [0.0, big, 1.0]])
2654            .unwrap();
2655
2656        assert_unrepresentable(
2657            &m.det_exact_rounded_f64(),
2658            None,
2659            UnrepresentableReason::NotFinite,
2660        );
2661    }
2662
2663    #[test]
2664    fn det_exact_f64_underflow_returns_err_for_nonzero_exact_result() {
2665        let tiny = f64::from_bits(1);
2666        let m = Matrix::<2>::try_from_rows([[tiny, 0.0], [0.0, tiny]]).unwrap();
2667
2668        assert!(m.det_exact().unwrap().is_positive());
2669        assert_unrepresentable(
2670            &m.det_exact_f64(),
2671            None,
2672            UnrepresentableReason::RequiresRounding,
2673        );
2674    }
2675
2676    #[test]
2677    fn det_exact_f64_rejects_inexact_rounding() {
2678        let m = Matrix::<2>::try_from_rows([[1.0 + f64::EPSILON, 0.0], [0.0, 1.0 - f64::EPSILON]])
2679            .unwrap();
2680
2681        assert_eq!(
2682            m.det_exact(),
2683            Ok(BigRational::new(
2684                (BigInt::from(1_u128) << 104_u32) - BigInt::from(1),
2685                BigInt::from(1_u128 << 104),
2686            ))
2687        );
2688        assert_unrepresentable(
2689            &m.det_exact_f64(),
2690            None,
2691            UnrepresentableReason::RequiresRounding,
2692        );
2693    }
2694
2695    #[test]
2696    fn det_exact_f64_accepts_max_finite_binary64() {
2697        let m = Matrix::<1>::try_from_rows([[f64::MAX]]).unwrap();
2698
2699        assert_eq!(m.det_exact_f64().unwrap().to_bits(), f64::MAX.to_bits());
2700    }
2701
2702    // -----------------------------------------------------------------------
2703    // solve_exact: macro-generated per-dimension tests (D=2..5)
2704    // -----------------------------------------------------------------------
2705
2706    /// Helper: build an arbitrary RHS vector for dimension `$d`.
2707    fn arbitrary_rhs<const D: usize>() -> Vector<D> {
2708        let values = [1.0, -2.5, 3.0, 0.25, -4.0];
2709        let mut arr = [0.0f64; D];
2710        for (dst, src) in arr.iter_mut().zip(values.iter()) {
2711            *dst = *src;
2712        }
2713        Vector::<D>::new(arr)
2714    }
2715
2716    macro_rules! gen_solve_exact_tests {
2717        ($d:literal) => {
2718            paste! {
2719                #[test]
2720                fn [<solve_exact_identity_paths_ $d d>]() {
2721                    let a = Matrix::<$d>::identity();
2722                    let b = arbitrary_rhs::<$d>();
2723                    let exact = a.solve_exact(b).unwrap();
2724                    let strict_f64 = a.solve_exact_f64(b).unwrap().into_array();
2725
2726                    for i in 0..$d {
2727                        assert_eq!(exact[i], f64_to_big_rational(b.as_array()[i]));
2728                        assert_eq!(strict_f64[i].to_bits(), b.as_array()[i].to_bits());
2729                    }
2730                }
2731
2732                #[test]
2733                fn [<solve_exact_singular_ $d d>]() {
2734                    // Zero matrix is singular.
2735                    let a = Matrix::<$d>::zero();
2736                    let b = arbitrary_rhs::<$d>();
2737                    assert_matches!(
2738                        a.solve_exact(b),
2739                        Err(LaError::Singular {
2740                            pivot_col: 0,
2741                            reason: SingularityReason::Exact,
2742                            ..
2743                        })
2744                    );
2745                }
2746            }
2747        };
2748    }
2749
2750    gen_solve_exact_tests!(2);
2751    gen_solve_exact_tests!(3);
2752    gen_solve_exact_tests!(4);
2753    gen_solve_exact_tests!(5);
2754
2755    /// For D ≤ 4, `solve_exact_f64` should agree with `Lu::solve` on
2756    /// well-conditioned matrices.
2757    macro_rules! gen_solve_exact_f64_agrees_with_lu {
2758        ($d:literal) => {
2759            paste! {
2760                #[test]
2761                fn [<solve_exact_f64_agrees_with_lu_ $d d>]() {
2762                    // Diagonally dominant integer matrix with an exactly
2763                    // representable target solution.  The exact result can
2764                    // therefore be parsed into f64 without rounding.
2765                    let mut rows = [[0.0f64; $d]; $d];
2766                    for r in 0..$d {
2767                        for c in 0..$d {
2768                            rows[r][c] = if r == c {
2769                                f64::from($d) + 1.0
2770                            } else {
2771                                1.0
2772                            };
2773                        }
2774                    }
2775                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
2776                    let x_true = {
2777                        let mut arr = [0.0f64; $d];
2778                        for (dst, src) in arr.iter_mut().zip([1.0, -2.0, 3.0, -4.0, 5.0]) {
2779                            *dst = src;
2780                        }
2781                        arr
2782                    };
2783                    let mut b_arr = [0.0f64; $d];
2784                    for i in 0..$d {
2785                        let mut sum = 0.0;
2786                        for j in 0..$d {
2787                            sum = rows[i][j].mul_add(x_true[j], sum);
2788                        }
2789                        b_arr[i] = sum;
2790                    }
2791                    let b = Vector::<$d>::new(b_arr);
2792                    let exact = a.solve_exact_f64(b).unwrap().into_array();
2793                    let lu_sol = a.lu(DEFAULT_SINGULAR_TOL).unwrap()
2794                        .solve(b).unwrap().into_array();
2795                    for i in 0..$d {
2796                        assert_eq!(exact[i].to_bits(), x_true[i].to_bits());
2797                        let eps = lu_sol[i].abs().mul_add(1e-12, 1e-12);
2798                        assert!((exact[i] - lu_sol[i]).abs() <= eps);
2799                    }
2800                }
2801            }
2802        };
2803    }
2804
2805    gen_solve_exact_f64_agrees_with_lu!(2);
2806    gen_solve_exact_f64_agrees_with_lu!(3);
2807    gen_solve_exact_f64_agrees_with_lu!(4);
2808    gen_solve_exact_f64_agrees_with_lu!(5);
2809
2810    /// Round-trip: for a well-conditioned integer matrix `A` and integer
2811    /// target `x0`, solving `A x = A x0` must return `x0` exactly.  All
2812    /// intermediate values stay small enough that `A * x0` is exactly
2813    /// representable in `f64`, so the round-trip is a precise equality
2814    /// check on the hybrid BigInt/BigRational path.
2815    macro_rules! gen_solve_exact_roundtrip_tests {
2816        ($d:literal) => {
2817            paste! {
2818                #[test]
2819                #[expect(
2820                    clippy::cast_precision_loss,
2821                    reason = "dimensions and indices are at most five and exactly representable as f64"
2822                )]
2823                fn [<solve_exact_roundtrip_ $d d>]() {
2824                    // A = D * I + J (diag = D+1, off-diag = 1).  Invertible
2825                    // for any D >= 1 and cheap to multiply by hand.
2826                    let mut rows = [[0.0f64; $d]; $d];
2827                    for r in 0..$d {
2828                        for c in 0..$d {
2829                            rows[r][c] = if r == c {
2830                                f64::from($d) + 1.0
2831                            } else {
2832                                1.0
2833                            };
2834                        }
2835                    }
2836                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
2837
2838                    // x0 = [1, 2, ..., D].
2839                    let mut x0 = [0.0f64; $d];
2840                    for i in 0..$d {
2841                        x0[i] = (i + 1) as f64;
2842                    }
2843
2844                    // b = A * x0 computed in f64.  With small integers the
2845                    // multiply-add sequence is exact.
2846                    let mut b_arr = [0.0f64; $d];
2847                    for r in 0..$d {
2848                        let mut sum = 0.0_f64;
2849                        for c in 0..$d {
2850                            sum = rows[r][c].mul_add(x0[c], sum);
2851                        }
2852                        b_arr[r] = sum;
2853                    }
2854                    let b = Vector::<$d>::new(b_arr);
2855
2856                    let x = a.solve_exact(b).unwrap();
2857                    for i in 0..$d {
2858                        assert_eq!(x[i], f64_to_big_rational(x0[i]));
2859                    }
2860                }
2861            }
2862        };
2863    }
2864
2865    gen_solve_exact_roundtrip_tests!(2);
2866    gen_solve_exact_roundtrip_tests!(3);
2867    gen_solve_exact_roundtrip_tests!(4);
2868    gen_solve_exact_roundtrip_tests!(5);
2869
2870    // -----------------------------------------------------------------------
2871    // solve_exact: dimension-specific tests
2872    // -----------------------------------------------------------------------
2873
2874    #[test]
2875    fn solve_exact_d0_returns_empty() {
2876        let a = Matrix::<0>::zero();
2877        let b = Vector::<0>::zero();
2878        let x = a.solve_exact(b).unwrap();
2879        assert!(x.is_empty());
2880    }
2881
2882    #[test]
2883    fn solve_exact_known_2x2() {
2884        // [[1,2],[3,4]] x = [5, 11] → x = [1, 2]
2885        let a = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
2886        let b = Vector::<2>::new([5.0, 11.0]);
2887        let x = a.solve_exact(b).unwrap();
2888        assert_eq!(x[0], BigRational::from_integer(BigInt::from(1)));
2889        assert_eq!(x[1], BigRational::from_integer(BigInt::from(2)));
2890    }
2891
2892    #[test]
2893    fn solve_exact_pivoting_needed() {
2894        // First column has zero on diagonal → pivot swap required.
2895        let a = Matrix::<3>::try_from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
2896            .unwrap();
2897        let b = Vector::<3>::new([2.0, 3.0, 4.0]);
2898        let x = a.solve_exact(b).unwrap();
2899        // x = [3, 2, 4]
2900        assert_eq!(x[0], f64_to_big_rational(3.0));
2901        assert_eq!(x[1], f64_to_big_rational(2.0));
2902        assert_eq!(x[2], f64_to_big_rational(4.0));
2903    }
2904
2905    #[test]
2906    fn solve_exact_fractional_result() {
2907        // [[2, 1], [1, 3]] x = [1, 1] → x = [2/5, 1/5]
2908        let a = Matrix::<2>::try_from_rows([[2.0, 1.0], [1.0, 3.0]]).unwrap();
2909        let b = Vector::<2>::new([1.0, 1.0]);
2910        let x = a.solve_exact(b).unwrap();
2911        assert_eq!(x[0], BigRational::new(BigInt::from(2), BigInt::from(5)));
2912        assert_eq!(x[1], BigRational::new(BigInt::from(1), BigInt::from(5)));
2913    }
2914
2915    #[test]
2916    fn solve_exact_singular_duplicate_rows() {
2917        let a = Matrix::<3>::try_from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [1.0, 2.0, 3.0]])
2918            .unwrap();
2919        let b = Vector::<3>::new([1.0, 2.0, 3.0]);
2920        assert_matches!(
2921            a.solve_exact(b),
2922            Err(LaError::Singular {
2923                reason: SingularityReason::Exact,
2924                ..
2925            })
2926        );
2927    }
2928
2929    #[test]
2930    fn solve_exact_5x5_permutation() {
2931        // Permutation matrix (swap rows 0↔1): P x = b → x = P^T b.
2932        let a = Matrix::<5>::try_from_rows([
2933            [0.0, 1.0, 0.0, 0.0, 0.0],
2934            [1.0, 0.0, 0.0, 0.0, 0.0],
2935            [0.0, 0.0, 1.0, 0.0, 0.0],
2936            [0.0, 0.0, 0.0, 1.0, 0.0],
2937            [0.0, 0.0, 0.0, 0.0, 1.0],
2938        ])
2939        .unwrap();
2940        let b = Vector::<5>::new([10.0, 20.0, 30.0, 40.0, 50.0]);
2941        let x = a.solve_exact(b).unwrap();
2942        assert_eq!(x[0], f64_to_big_rational(20.0));
2943        assert_eq!(x[1], f64_to_big_rational(10.0));
2944        assert_eq!(x[2], f64_to_big_rational(30.0));
2945        assert_eq!(x[3], f64_to_big_rational(40.0));
2946        assert_eq!(x[4], f64_to_big_rational(50.0));
2947    }
2948
2949    /// Entries near `f64::MAX / 2` are finite but their product would
2950    /// overflow to ±∞ in pure f64 arithmetic.  The `BigInt` augmented-system
2951    /// path computes the correct solution without any overflow.  The D×D
2952    /// case uses a diagonal matrix with `big` on every diagonal and a RHS
2953    /// of `[big, …, big, 0]`, giving the known solution `[1, …, 1, 0]`.
2954    macro_rules! gen_solve_exact_large_finite_entries_tests {
2955        ($d:literal) => {
2956            paste! {
2957                #[test]
2958                fn [<solve_exact_large_finite_entries_ $d d>]() {
2959                    let big = f64::MAX / 2.0;
2960                    assert!(big.is_finite());
2961                    // D×D diagonal matrix with `big` on the diagonal.
2962                    let mut rows = [[0.0f64; $d]; $d];
2963                    for i in 0..$d {
2964                        rows[i][i] = big;
2965                    }
2966                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
2967                    // RHS = [big, …, big, 0] → x = [1, …, 1, 0].
2968                    let mut b_arr = [big; $d];
2969                    b_arr[$d - 1] = 0.0;
2970                    let b = Vector::<$d>::new(b_arr);
2971                    let x = a.solve_exact(b).unwrap();
2972                    for i in 0..($d - 1) {
2973                        assert_eq!(x[i], BigRational::from_integer(BigInt::from(1)));
2974                    }
2975                    assert_eq!(x[$d - 1], BigRational::from_integer(BigInt::from(0)));
2976                }
2977            }
2978        };
2979    }
2980
2981    gen_solve_exact_large_finite_entries_tests!(2);
2982    gen_solve_exact_large_finite_entries_tests!(3);
2983    gen_solve_exact_large_finite_entries_tests!(4);
2984    gen_solve_exact_large_finite_entries_tests!(5);
2985
2986    /// Matrix and RHS entries span many orders of magnitude (from
2987    /// `f64::MIN_POSITIVE` up through `1e100`). This exercises each
2988    /// collection's independently derived minimum exponent: even the largest
2989    /// within-collection shift remains a representable `BigInt`. The D×D case
2990    /// alternates `huge`/`tiny` along the diagonal with a matching RHS, giving
2991    /// `x = [1, …, 1]`.
2992    macro_rules! gen_solve_exact_mixed_magnitude_entries_tests {
2993        ($d:literal) => {
2994            paste! {
2995                #[test]
2996                fn [<solve_exact_mixed_magnitude_entries_ $d d>]() {
2997                    let tiny = f64::MIN_POSITIVE; // 2^-1022, smallest normal
2998                    let huge = 1.0e100_f64;
2999                    // Alternate huge/tiny along the diagonal.
3000                    let mut rows = [[0.0f64; $d]; $d];
3001                    let mut b_arr = [0.0f64; $d];
3002                    for i in 0..$d {
3003                        let val = if i % 2 == 0 { huge } else { tiny };
3004                        rows[i][i] = val;
3005                        b_arr[i] = val;
3006                    }
3007                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
3008                    let b = Vector::<$d>::new(b_arr);
3009                    let x = a.solve_exact(b).unwrap();
3010                    for i in 0..$d {
3011                        assert_eq!(x[i], BigRational::from_integer(BigInt::from(1)));
3012                    }
3013                }
3014            }
3015        };
3016    }
3017
3018    gen_solve_exact_mixed_magnitude_entries_tests!(2);
3019    gen_solve_exact_mixed_magnitude_entries_tests!(3);
3020    gen_solve_exact_mixed_magnitude_entries_tests!(4);
3021    gen_solve_exact_mixed_magnitude_entries_tests!(5);
3022
3023    #[test]
3024    fn solve_exact_restores_independent_matrix_and_rhs_scales() {
3025        let large = 2.0_f64.powi(500);
3026        let tiny = 2.0_f64.powi(-1000);
3027
3028        let large_matrix = Matrix::<2>::try_from_rows([[large, 0.0], [0.0, large]]).unwrap();
3029        let tiny_rhs = Vector::<2>::new([tiny, -2.0 * tiny]);
3030        let small_solution = large_matrix.solve_exact(tiny_rhs).unwrap();
3031        assert_eq!(
3032            small_solution[0],
3033            BigRational::new(BigInt::from(1_u8), BigInt::from(1_u8) << 1500_u32)
3034        );
3035        assert_eq!(
3036            small_solution[1],
3037            BigRational::new(BigInt::from(-1_i8), BigInt::from(1_u8) << 1499_u32)
3038        );
3039
3040        let tiny_matrix = Matrix::<1>::try_from_rows([[tiny]]).unwrap();
3041        let large_rhs = Vector::<1>::new([large]);
3042        let large_solution = tiny_matrix.solve_exact(large_rhs).unwrap();
3043        assert_eq!(
3044            large_solution[0],
3045            BigRational::from_integer(BigInt::from(1_u8) << 1500_u32)
3046        );
3047    }
3048
3049    /// Subnormal RHS entries must survive the decomposition and
3050    /// back-substitution paths unchanged.  The D×D case uses the identity
3051    /// matrix and RHS `[1·tiny, 2·tiny, …, D·tiny]`; each entry remains a
3052    /// valid subnormal f64 (integer multiples of `2^-1074` fit in the
3053    /// 52-bit subnormal mantissa for the small integers used here).
3054    macro_rules! gen_solve_exact_subnormal_rhs_tests {
3055        ($d:literal) => {
3056            paste! {
3057                #[test]
3058                #[expect(
3059                    clippy::cast_precision_loss,
3060                    reason = "indices are at most five and exactly representable as f64"
3061                )]
3062                fn [<solve_exact_subnormal_rhs_ $d d>]() {
3063                    let tiny = 5e-324_f64; // smallest positive subnormal
3064                    assert!(tiny.is_subnormal());
3065                    let a = Matrix::<$d>::identity();
3066                    // b[i] = (i+1) · tiny — each entry remains a valid subnormal.
3067                    let mut b_arr = [0.0f64; $d];
3068                    for i in 0..$d {
3069                        b_arr[i] = (i + 1) as f64 * tiny;
3070                        assert!(b_arr[i].is_subnormal());
3071                    }
3072                    let b = Vector::<$d>::new(b_arr);
3073                    let x = a.solve_exact(b).unwrap();
3074                    for i in 0..$d {
3075                        assert_eq!(x[i], f64_to_big_rational((i + 1) as f64 * tiny));
3076                    }
3077                }
3078            }
3079        };
3080    }
3081
3082    gen_solve_exact_subnormal_rhs_tests!(2);
3083    gen_solve_exact_subnormal_rhs_tests!(3);
3084    gen_solve_exact_subnormal_rhs_tests!(4);
3085    gen_solve_exact_subnormal_rhs_tests!(5);
3086
3087    /// Pivoting path with a zero top-left entry forces a row swap in the
3088    /// `BigInt` forward-elimination loop and propagates it to the RHS.
3089    /// Combined with a fractional solution, this exercises the
3090    /// `BigRational` back-substitution after integer forward elimination.
3091    ///
3092    /// The 2×2 block `[[0, 1], [2, 1]]` with rhs `[3, 4]` (→ `x = [1/2, 3]`)
3093    /// is embedded into the top-left of a D×D identity matrix.  Remaining
3094    /// rows contribute pass-through equalities `x[i] = b[i]`, so the same
3095    /// fractional solution appears at indices 0 and 1 regardless of D.
3096    macro_rules! gen_solve_exact_pivot_swap_fractional_tests {
3097        ($d:literal) => {
3098            paste! {
3099                #[test]
3100                #[expect(
3101                    clippy::cast_precision_loss,
3102                    reason = "indices and test offsets are small integers exactly representable as f64"
3103                )]
3104                fn [<solve_exact_pivot_swap_with_fractional_result_ $d d>]() {
3105                    // Top-left 2×2: A = [[0, 1], [2, 1]].  After swap:
3106                    // [[2, 1], [0, 1]], rhs = [4, 3] → x[1] = 3, x[0] = 1/2.
3107                    let mut rows = [[0.0f64; $d]; $d];
3108                    rows[0][1] = 1.0;
3109                    rows[1][0] = 2.0;
3110                    rows[1][1] = 1.0;
3111                    // Identity padding for the remaining rows.
3112                    for (i, row) in rows.iter_mut().enumerate().skip(2) {
3113                        row[i] = 1.0;
3114                    }
3115                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
3116                    // b = [3, 4, 12, 13, …]; padded entries are arbitrary
3117                    // finite integers so the identity block gives x[i] = b[i].
3118                    let mut b_arr = [0.0f64; $d];
3119                    b_arr[0] = 3.0;
3120                    b_arr[1] = 4.0;
3121                    for (i, value) in b_arr.iter_mut().enumerate().skip(2) {
3122                        *value = (i + 10) as f64;
3123                    }
3124                    let b = Vector::<$d>::new(b_arr);
3125                    let x = a.solve_exact(b).unwrap();
3126                    assert_eq!(x[0], BigRational::new(BigInt::from(1), BigInt::from(2)));
3127                    assert_eq!(x[1], BigRational::from_integer(BigInt::from(3)));
3128                    for (i, value) in x.iter().enumerate().skip(2) {
3129                        assert_eq!(value, &f64_to_big_rational((i + 10) as f64));
3130                    }
3131                }
3132            }
3133        };
3134    }
3135
3136    gen_solve_exact_pivot_swap_fractional_tests!(2);
3137    gen_solve_exact_pivot_swap_fractional_tests!(3);
3138    gen_solve_exact_pivot_swap_fractional_tests!(4);
3139    gen_solve_exact_pivot_swap_fractional_tests!(5);
3140
3141    /// Mid-elimination pivot swap: the 3×3 block
3142    /// `[[1, 2, 3], [0, 0, 4], [0, 5, 6]]` has a non-zero pivot at k=0 but
3143    /// a zero pivot at k=1, so the swap happens *during* forward
3144    /// elimination rather than at the start.  With rhs `[6, 7, 8]` the
3145    /// exact solution is `[7/4, -1/2, 7/4]`.  For D > 3 the block is
3146    /// embedded into the top-left of a D×D identity matrix so the same
3147    /// fractional solution appears in `x[0..3]` and `x[i] = b[i]` for
3148    /// `i >= 3`.
3149    macro_rules! gen_solve_exact_mid_pivot_swap_tests {
3150        ($d:literal) => {
3151            paste! {
3152                #[test]
3153                #[expect(
3154                    clippy::cast_precision_loss,
3155                    reason = "indices and test offsets are small integers exactly representable as f64"
3156                )]
3157                fn [<solve_exact_mid_pivot_swap_ $d d>]() {
3158                    let mut rows = [[0.0f64; $d]; $d];
3159                    rows[0][0] = 1.0; rows[0][1] = 2.0; rows[0][2] = 3.0;
3160                    // rows[1][0..2] are zero; rows[1][2] = 4.
3161                    rows[1][2] = 4.0;
3162                    rows[2][1] = 5.0; rows[2][2] = 6.0;
3163                    // Identity padding for the remaining rows.
3164                    for (i, row) in rows.iter_mut().enumerate().skip(3) {
3165                        row[i] = 1.0;
3166                    }
3167                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
3168                    let mut b_arr = [0.0f64; $d];
3169                    b_arr[0] = 6.0;
3170                    b_arr[1] = 7.0;
3171                    b_arr[2] = 8.0;
3172                    for (i, value) in b_arr.iter_mut().enumerate().skip(3) {
3173                        *value = (i + 10) as f64;
3174                    }
3175                    let b = Vector::<$d>::new(b_arr);
3176                    let x = a.solve_exact(b).unwrap();
3177                    // x[0..3] = [7/4, -1/2, 7/4].
3178                    assert_eq!(x[0], BigRational::new(BigInt::from(7), BigInt::from(4)));
3179                    assert_eq!(x[1], BigRational::new(BigInt::from(-1), BigInt::from(2)));
3180                    assert_eq!(x[2], BigRational::new(BigInt::from(7), BigInt::from(4)));
3181                    for (i, value) in x.iter().enumerate().skip(3) {
3182                        assert_eq!(value, &f64_to_big_rational((i + 10) as f64));
3183                    }
3184                }
3185            }
3186        };
3187    }
3188
3189    gen_solve_exact_mid_pivot_swap_tests!(3);
3190    gen_solve_exact_mid_pivot_swap_tests!(4);
3191    gen_solve_exact_mid_pivot_swap_tests!(5);
3192
3193    /// Rank-deficient singular: the last column is identically zero and the
3194    /// leading `(D-1)×(D-1)` block is full rank, so every intermediate
3195    /// pivot is non-zero and the singularity surfaces only at the final
3196    /// column.  The matrix is identity in the top-left `(D-1)×(D-1)` with
3197    /// a row of ones as the last row (and an all-zero last column), so the
3198    /// rank is exactly `D-1`.  `solve_exact` must return
3199    /// exact singularity at `pivot_col = D - 1`.
3200    macro_rules! gen_solve_exact_singular_rank_deficient_tests {
3201        ($d:literal) => {
3202            paste! {
3203                #[test]
3204                fn [<solve_exact_singular_rank_deficient_ $d d>]() {
3205                    let mut rows = [[0.0f64; $d]; $d];
3206                    for i in 0..($d - 1) {
3207                        rows[i][i] = 1.0;
3208                        rows[$d - 1][i] = 1.0;
3209                    }
3210                    // Last column is left all-zero → rank exactly D-1.
3211                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
3212                    let b = Vector::<$d>::new([1.0; $d]);
3213                    assert_matches!(
3214                        a.solve_exact(b),
3215                        Err(LaError::Singular {
3216                            pivot_col,
3217                            reason: SingularityReason::Exact,
3218                            ..
3219                        }) if pivot_col == $d - 1
3220                    );
3221                }
3222            }
3223        };
3224    }
3225
3226    gen_solve_exact_singular_rank_deficient_tests!(2);
3227    gen_solve_exact_singular_rank_deficient_tests!(3);
3228    gen_solve_exact_singular_rank_deficient_tests!(4);
3229    gen_solve_exact_singular_rank_deficient_tests!(5);
3230
3231    /// Zero RHS with a non-singular matrix.  Every Bareiss update reads
3232    /// `rhs[k]` and `rhs[i]`, both initialised to zero; every update
3233    /// produces zero; back-substitution therefore yields `x = 0`
3234    /// regardless of the matrix entries.  This exercises the
3235    /// back-substitution `mem::take` path against an all-zero `rhs`.
3236    macro_rules! gen_solve_exact_zero_rhs_tests {
3237        ($d:literal) => {
3238            paste! {
3239                #[test]
3240                fn [<solve_exact_zero_rhs_ $d d>]() {
3241                    // A = D*I + J (diagonally dominant, invertible).
3242                    let mut rows = [[1.0f64; $d]; $d];
3243                    for i in 0..$d {
3244                        rows[i][i] = f64::from($d) + 1.0;
3245                    }
3246                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
3247                    let b = Vector::<$d>::zero();
3248                    let x = a.solve_exact(b).unwrap();
3249                    for xi in &x {
3250                        assert_eq!(*xi, BigRational::from_integer(BigInt::from(0)));
3251                    }
3252                }
3253            }
3254        };
3255    }
3256
3257    gen_solve_exact_zero_rhs_tests!(2);
3258    gen_solve_exact_zero_rhs_tests!(3);
3259    gen_solve_exact_zero_rhs_tests!(4);
3260    gen_solve_exact_zero_rhs_tests!(5);
3261
3262    // -----------------------------------------------------------------------
3263    // Adversarial-input coverage mirroring `benches/exact.rs`
3264    // -----------------------------------------------------------------------
3265    //
3266    // These tests pin the behaviour of the extreme-input benchmark groups
3267    // (`exact_near_singular_3x3`, `exact_large_entries_3x3`,
3268    // `exact_hilbert_{4x4,5x5}`) so a regression would be caught even
3269    // when benchmarks are not running.
3270
3271    /// Multiply `A · x` entirely in `BigRational`, using `f64_to_big_rational`
3272    /// to lift each matrix entry.  Used by residual assertions for inputs
3273    /// whose exact solution has no closed form we can easily type out.
3274    fn big_rational_matvec<const D: usize>(
3275        a: &Matrix<D>,
3276        x: &[BigRational; D],
3277    ) -> [BigRational; D] {
3278        from_fn(|i| {
3279            let mut sum = BigRational::from_integer(BigInt::from(0));
3280            for (aij, xj) in a.as_rows()[i].iter().zip(x.iter()) {
3281                sum += f64_to_big_rational(*aij) * xj;
3282            }
3283            sum
3284        })
3285    }
3286
3287    fn hilbert<const D: usize>() -> Matrix<D> {
3288        let rows = from_fn(|r| from_fn(|c| 1.0 / f64::from(u32::try_from(r + c + 1).unwrap())));
3289        Matrix::<D>::try_from_rows(rows).unwrap()
3290    }
3291
3292    /// Near-singular 3×3 solve (matches the `exact_near_singular_3x3`
3293    /// bench).  With `A = [[1+2^-50, 2, 3], [4, 5, 6], [7, 8, 9]]` and
3294    /// `x0 = [1, 1, 1]`, `A · x0 = [6 + 2^-50, 15, 24]`; every component is
3295    /// exactly representable in `f64` (`6` has ulp `2^-50` at its exponent).
3296    /// `solve_exact` must recover `x0` exactly — the fractional denominator
3297    /// introduced by `det(A) = -3 × 2^-50` cancels cleanly against the
3298    /// augmented RHS.
3299    #[test]
3300    fn solve_exact_near_singular_3x3_integer_x0() {
3301        let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); // 2^-50
3302        let a = Matrix::<3>::try_from_rows([
3303            [1.0 + perturbation, 2.0, 3.0],
3304            [4.0, 5.0, 6.0],
3305            [7.0, 8.0, 9.0],
3306        ])
3307        .unwrap();
3308        let b = Vector::<3>::new([6.0 + perturbation, 15.0, 24.0]);
3309        let x = a.solve_exact(b).unwrap();
3310        let one = BigRational::from_integer(BigInt::from(1));
3311        assert_eq!(x[0], one);
3312        assert_eq!(x[1], one);
3313        assert_eq!(x[2], one);
3314    }
3315
3316    /// Large-entry 3×3 solve (matches the `exact_large_entries_3x3`
3317    /// bench).  `A = big · I + (1 - I)` with `big = f64::MAX / 2` and
3318    /// `b = [big, 1, 1] = A · [1, 0, 0]`.  The `BigInt` augmented system
3319    /// sees entries of ~1023 bits on the diagonal and unit entries
3320    /// elsewhere; Bareiss elimination still produces the exact integer
3321    /// solution `[1, 0, 0]`.
3322    #[test]
3323    fn solve_exact_large_entries_3x3_unit_vector() {
3324        let big = f64::MAX / 2.0;
3325        assert!(big.is_finite());
3326        let a = Matrix::<3>::try_from_rows([[big, 1.0, 1.0], [1.0, big, 1.0], [1.0, 1.0, big]])
3327            .unwrap();
3328        let b = Vector::<3>::new([big, 1.0, 1.0]);
3329        let x = a.solve_exact(b).unwrap();
3330        let zero = BigRational::from_integer(BigInt::from(0));
3331        let one = BigRational::from_integer(BigInt::from(1));
3332        assert_eq!(x[0], one);
3333        assert_eq!(x[1], zero);
3334        assert_eq!(x[2], zero);
3335    }
3336
3337    /// Determinant of the large-entry 3×3 is roughly `big^3`, which
3338    /// overflows `f64`. `det_direct()` therefore reports a computed
3339    /// [`LaError::NonFinite`], the fast filter inside `det_sign_exact`
3340    /// treats that as inconclusive, and the direct `BigInt` fallback resolves
3341    /// the positive sign correctly. `det_exact_f64` must report `Unrepresentable`.
3342    #[test]
3343    fn det_sign_exact_large_entries_3x3_positive() {
3344        let big = f64::MAX / 2.0;
3345        let a = Matrix::<3>::try_from_rows([[big, 1.0, 1.0], [1.0, big, 1.0], [1.0, 1.0, big]])
3346            .unwrap();
3347        // Fast filter is inconclusive (big^3 overflows f64 to +∞), so
3348        // this exercises the direct `BigInt` cold path.
3349        assert_matches!(
3350            a.det_direct(),
3351            Err(LaError::NonFinite {
3352                location: NonFiniteLocation::Scalar,
3353                origin: NonFiniteOrigin::Computation {
3354                    operation: ArithmeticOperation::Determinant,
3355                    ..
3356                },
3357                ..
3358            })
3359        );
3360        assert_eq!(a.det_sign_exact(), DeterminantSign::Positive);
3361        // Cross-validate: the exact `BigRational` determinant must agree
3362        // on sign with `det_sign_exact`, and `det_exact_f64` must reject the
3363        // conversion (the value is representable in BigRational but far exceeds f64).
3364        assert!(a.det_exact().unwrap().is_positive());
3365        assert_unrepresentable(&a.det_exact_f64(), None, UnrepresentableReason::NotFinite);
3366    }
3367
3368    /// Hilbert matrices are symmetric positive-definite, so
3369    /// `det_sign_exact` must return [`DeterminantSign::Positive`] for every D.
3370    /// For D=2..=4 the
3371    /// fast f64 filter resolves the positive sign without falling
3372    /// through (Hilbert's determinant is tiny but still well above the
3373    /// `det_errbound` cushion); for D=5 the filter is skipped entirely
3374    /// and the Bareiss path handles inputs whose `(mantissa, exponent)`
3375    /// pairs all differ.
3376    macro_rules! gen_det_sign_exact_hilbert_positive_tests {
3377        ($d:literal) => {
3378            paste! {
3379                #[test]
3380                fn [<det_sign_exact_hilbert_positive_ $d d>]() {
3381                    let h = hilbert::<$d>();
3382                    assert_eq!(h.det_sign_exact(), DeterminantSign::Positive);
3383                }
3384            }
3385        };
3386    }
3387
3388    gen_det_sign_exact_hilbert_positive_tests!(2);
3389    gen_det_sign_exact_hilbert_positive_tests!(3);
3390    gen_det_sign_exact_hilbert_positive_tests!(4);
3391    gen_det_sign_exact_hilbert_positive_tests!(5);
3392
3393    /// `solve_exact` on a Hilbert matrix must produce a solution whose
3394    /// residual `A · x - b` is *exactly* zero in `BigRational` arithmetic.
3395    /// Hilbert entries (`1/3`, `1/5`, `1/6`, `1/7`, …) are non-terminating
3396    /// in binary, so this is a stronger test than the
3397    /// `gen_solve_exact_roundtrip_tests` construction (which requires the
3398    /// RHS to be representable as an exact `f64` product).
3399    macro_rules! gen_solve_exact_hilbert_residual_tests {
3400        ($d:literal) => {
3401            paste! {
3402                #[test]
3403                fn [<solve_exact_hilbert_residual_ $d d>]() {
3404                    let h = hilbert::<$d>();
3405                    // Use a non-trivial RHS with both positive and negative
3406                    // entries to avoid accidental structural cancellation.
3407                    let mut b_arr = [0.0f64; $d];
3408                    for i in 0usize..$d {
3409                        let sign = if i.is_multiple_of(2) { 1.0 } else { -1.0 };
3410                        b_arr[i] = sign * f64::from(u32::try_from(i + 1).unwrap());
3411                    }
3412                    let b = Vector::<$d>::new(b_arr);
3413                    let x = h.solve_exact(b).unwrap();
3414                    let ax = big_rational_matvec(&h, &x);
3415                    for i in 0..$d {
3416                        assert_eq!(ax[i], f64_to_big_rational(b_arr[i]));
3417                    }
3418                }
3419            }
3420        };
3421    }
3422
3423    gen_solve_exact_hilbert_residual_tests!(2);
3424    gen_solve_exact_hilbert_residual_tests!(3);
3425    gen_solve_exact_hilbert_residual_tests!(4);
3426    gen_solve_exact_hilbert_residual_tests!(5);
3427
3428    // -----------------------------------------------------------------------
3429    // solve_exact_f64: dimension-specific tests
3430    // -----------------------------------------------------------------------
3431
3432    #[test]
3433    fn solve_exact_f64_known_2x2() {
3434        let a = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
3435        let b = Vector::<2>::new([5.0, 11.0]);
3436        let x = a.solve_exact_f64(b).unwrap().into_array();
3437        assert!((x[0] - 1.0).abs() <= f64::EPSILON);
3438        assert!((x[1] - 2.0).abs() <= f64::EPSILON);
3439    }
3440
3441    #[test]
3442    fn solve_exact_f64_overflow_returns_err() {
3443        // [[1/big, 0], [0, 1/big]] x = [big, big] → x = [big², big²],
3444        // which overflows f64.
3445        let big = f64::MAX / 2.0;
3446        let a = Matrix::<2>::try_from_rows([[1.0 / big, 0.0], [0.0, 1.0 / big]]).unwrap();
3447        let b = Vector::<2>::new([big, big]);
3448        assert_unrepresentable(
3449            &a.solve_exact_f64(b),
3450            Some(0),
3451            UnrepresentableReason::NotFinite,
3452        );
3453    }
3454
3455    #[test]
3456    fn solve_exact_f64_huge_non_dyadic_component_returns_not_finite() {
3457        let a = Matrix::<1>::try_from_rows([[3.0 * f64::MIN_POSITIVE]]).unwrap();
3458        let b = Vector::<1>::new([f64::MAX]);
3459
3460        assert_unrepresentable(
3461            &a.solve_exact_f64(b),
3462            Some(0),
3463            UnrepresentableReason::NotFinite,
3464        );
3465    }
3466
3467    #[test]
3468    fn solve_exact_rounded_f64_overflow_returns_err() {
3469        let big = f64::MAX / 2.0;
3470        let a = Matrix::<2>::try_from_rows([[1.0 / big, 0.0], [0.0, 1.0 / big]]).unwrap();
3471        let b = Vector::<2>::new([big, big]);
3472
3473        assert_unrepresentable(
3474            &a.solve_exact_rounded_f64(b),
3475            Some(0),
3476            UnrepresentableReason::NotFinite,
3477        );
3478    }
3479
3480    #[test]
3481    fn solve_exact_f64_underflow_returns_err_for_nonzero_exact_component() {
3482        let tiny = f64::from_bits(1);
3483        let a = Matrix::<1>::try_from_rows([[2.0]]).unwrap();
3484        let b = Vector::<1>::new([tiny]);
3485
3486        assert_unrepresentable(
3487            &a.solve_exact_f64(b),
3488            Some(0),
3489            UnrepresentableReason::RequiresRounding,
3490        );
3491    }
3492
3493    #[test]
3494    fn solve_exact_f64_accepts_smallest_subnormal_result() {
3495        let tiny = f64::from_bits(1);
3496        let a = Matrix::<1>::identity();
3497        let b = Vector::<1>::new([tiny]);
3498
3499        assert_eq!(
3500            a.solve_exact_f64(b).unwrap().into_array()[0].to_bits(),
3501            tiny.to_bits()
3502        );
3503    }
3504
3505    // -----------------------------------------------------------------------
3506    // exact solve boundary tests
3507    // -----------------------------------------------------------------------
3508
3509    #[test]
3510    fn bareiss_solve_d1() {
3511        let a = Matrix::<1>::try_from_rows([[2.0]]).unwrap();
3512        let b = Vector::<1>::new([6.0]);
3513        let x = a.solve_exact(b).unwrap();
3514        assert_eq!(x[0], f64_to_big_rational(3.0));
3515    }
3516
3517    #[test]
3518    fn bareiss_solve_singular_column_all_zero() {
3519        let a = Matrix::<3>::try_from_rows([[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
3520            .unwrap();
3521        let b = Vector::<3>::new([1.0, 2.0, 3.0]);
3522        assert_matches!(
3523            a.solve_exact(b),
3524            Err(LaError::Singular {
3525                pivot_col: 1,
3526                reason: SingularityReason::Exact,
3527                ..
3528            })
3529        );
3530    }
3531
3532    // -----------------------------------------------------------------------
3533    // f64_to_big_rational tests
3534    // -----------------------------------------------------------------------
3535
3536    #[test]
3537    fn f64_to_big_rational_positive_zero() {
3538        let r = f64_to_big_rational(0.0);
3539        assert_eq!(r, BigRational::from_integer(BigInt::from(0)));
3540    }
3541
3542    #[test]
3543    fn f64_to_big_rational_negative_zero() {
3544        let r = f64_to_big_rational(-0.0);
3545        assert_eq!(r, BigRational::from_integer(BigInt::from(0)));
3546    }
3547
3548    #[test]
3549    fn f64_to_big_rational_one() {
3550        let r = f64_to_big_rational(1.0);
3551        assert_eq!(r, BigRational::from_integer(BigInt::from(1)));
3552    }
3553
3554    #[test]
3555    fn f64_to_big_rational_negative_one() {
3556        let r = f64_to_big_rational(-1.0);
3557        assert_eq!(r, BigRational::from_integer(BigInt::from(-1)));
3558    }
3559
3560    #[test]
3561    fn f64_to_big_rational_half() {
3562        let r = f64_to_big_rational(0.5);
3563        assert_eq!(r, BigRational::new(BigInt::from(1), BigInt::from(2)));
3564    }
3565
3566    #[test]
3567    fn f64_to_big_rational_quarter() {
3568        let r = f64_to_big_rational(0.25);
3569        assert_eq!(r, BigRational::new(BigInt::from(1), BigInt::from(4)));
3570    }
3571
3572    #[test]
3573    fn f64_to_big_rational_negative_three_and_a_half() {
3574        // -3.5 = -7/2
3575        let r = f64_to_big_rational(-3.5);
3576        assert_eq!(r, BigRational::new(BigInt::from(-7), BigInt::from(2)));
3577    }
3578
3579    #[test]
3580    fn f64_to_big_rational_integer() {
3581        let r = f64_to_big_rational(42.0);
3582        assert_eq!(r, BigRational::from_integer(BigInt::from(42)));
3583    }
3584
3585    #[test]
3586    fn f64_to_big_rational_power_of_two() {
3587        let r = f64_to_big_rational(1024.0);
3588        assert_eq!(r, BigRational::from_integer(BigInt::from(1024)));
3589    }
3590
3591    #[test]
3592    fn f64_to_big_rational_subnormal() {
3593        let tiny = 5e-324_f64; // smallest positive subnormal
3594        assert!(tiny.is_subnormal());
3595        let r = f64_to_big_rational(tiny);
3596        // 5e-324 = 1 × 2^(-1074)
3597        assert_eq!(
3598            r,
3599            BigRational::new(BigInt::from(1), BigInt::from(1u32) << 1074u32)
3600        );
3601    }
3602
3603    #[test]
3604    fn f64_to_big_rational_already_lowest_terms() {
3605        // 0.5 should produce numer=1, denom=2 (already reduced).
3606        let r = f64_to_big_rational(0.5);
3607        assert_eq!(*r.numer(), BigInt::from(1));
3608        assert_eq!(*r.denom(), BigInt::from(2));
3609    }
3610
3611    #[test]
3612    fn f64_to_big_rational_round_trip() {
3613        // -0.0 is excluded: it maps to BigRational(0) which round-trips
3614        // to +0.0 (correct; tested separately in f64_to_big_rational_negative_zero).
3615        let values = [
3616            0.0,
3617            1.0,
3618            -1.0,
3619            0.5,
3620            0.25,
3621            0.1,
3622            42.0,
3623            -3.5,
3624            1e10,
3625            1e-10,
3626            f64::MAX / 2.0,
3627            f64::MIN_POSITIVE,
3628            5e-324,
3629        ];
3630        for &v in &values {
3631            let r = f64_to_big_rational(v);
3632            let back = r.to_f64().expect("round-trip to_f64 failed");
3633            assert_eq!(
3634                v.to_bits(),
3635                back.to_bits(),
3636                "round-trip failed for {v}: got {back}"
3637            );
3638        }
3639    }
3640
3641    #[test]
3642    fn decompose_f64_rejects_non_finite_inputs() {
3643        for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
3644            assert_non_finite_input_scalar(&decompose_f64(value));
3645        }
3646    }
3647}