Skip to main content

ifc_lite_geometry/kernel/fixed_int/
mod.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! `FixedInt<const K: usize>` — a register-resident fixed-width signed big
6//! integer used as the arithmetic backing of the exact predicate tier
7//! (`kernel/fixed.rs`).
8//!
9//! ## Why a local newtype instead of `bnum`
10//! The fixed-width predicate cascade (`fixed_impl!`) is the FAST exact tier
11//! between the interval filter and the BigRational fallback; every LPI/TPI
12//! lambda and determinant runs its inner limb math here. `bnum` 0.14 backs its
13//! `Integer` with a `[u8; N]` byte array, so the hot add / sub / mul / neg on
14//! the narrow widths pay byte-granular work. `FixedInt` stores `[u64; K]`
15//! little-endian two's-complement limbs and hand-rolls the HOT ops as native
16//! 64-bit limb arithmetic (carry chains, a conservative-precheck schoolbook
17//! multiply, i64/i128 conversions, sign tests) so they stay in registers.
18//!
19//! ## Correctness contract (this is the whole game)
20//! Every operation the cascade relies on is **bit-identical** to the `bnum`
21//! type it replaces:
22//! - HOT ops are native but validated by an exhaustive differential fuzz
23//!   (`mod tests`) against `bnum` across all four widths (K = 4, 8, 16, 32),
24//!   every boundary value, and a deterministic LCG stream. `checked_mul` in
25//!   particular must agree with `bnum` on BOTH the value AND the `Some`/`None`
26//!   overflow verdict — a truncated product escaping as `Some` would silently
27//!   flip a predicate sign and corrupt CSG, so its precheck is deliberately
28//!   CONSERVATIVE (fast-accept only when the product provably fits, fast-reject
29//!   only when it provably overflows, exact wide check in the ambiguous band).
30//! - COLD ops (`Div`, `Rem`, `to_f64`, `from_str_radix`) delegate to `bnum`
31//!   through a little-endian two's-complement byte round-trip. `FixedInt<K>`'s
32//!   `K` u64 limbs are exactly `K*8` bytes, and `bnum::types::I{K*64}` stores
33//!   `K*8` bytes in the same little-endian two's-complement layout, so
34//!   `to_le_bytes`/`from_le_slice` is a lossless reinterpretation.
35//!
36//! The newtype is invisible to `fixed.rs`: the `fixed_impl!` macro names the
37//! width only through its `$T` type argument (`FixedInt<4/8/16/32>`), so no
38//! caller and no macro-body line changes.
39
40use core::cmp::Ordering;
41use core::ops::{Add, Div, Mul, Neg, Rem, Sub};
42use num_traits::{
43    CheckedAdd, CheckedMul, CheckedSub, FromPrimitive, Num, One, Signed, ToPrimitive, Zero,
44};
45
46mod mul;
47use mul::{mul_full, mul_low};
48
49/// Little-endian, two's-complement fixed-width signed integer with `K` u64
50/// limbs (`self.0[0]` is the least-significant limb; the sign bit is bit 63 of
51/// `self.0[K-1]`). Instantiated at K ∈ {4, 8, 16, 32} = I256/I512/I1024/I2048.
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub struct FixedInt<const K: usize>([u64; K]);
54
55// ── Inherent surface ────────────────────────────────────────────────────────
56// `is_zero` / `is_one` / `is_negative` / `is_positive` are provided as INHERENT
57// methods (not only via the `Zero`/`One`/`Signed` traits) because `bnum`
58// exposes them inherently and `fixed.rs` calls e.g. `x.is_zero()` /
59// `d.is_negative()` in scopes that import `Signed` but not `Zero`, or import no
60// predicate trait at all (`lambda1024`). Inherent methods take resolution
61// priority over trait methods, so these satisfy the macro calls and never
62// conflict with the trait impls below (which carry identical semantics).
63impl<const K: usize> FixedInt<K> {
64    /// Construct directly from little-endian limbs (test-only constructor).
65    #[cfg(test)]
66    #[inline]
67    pub(crate) const fn from_limbs(limbs: [u64; K]) -> Self {
68        FixedInt(limbs)
69    }
70
71    #[inline]
72    pub fn is_zero(&self) -> bool {
73        self.0.iter().all(|&l| l == 0)
74    }
75
76    #[inline]
77    pub fn is_one(&self) -> bool {
78        self.0[0] == 1 && self.0[1..].iter().all(|&l| l == 0)
79    }
80
81    /// Sign test. Takes `self` by value (mirroring `bnum`'s inherent
82    /// `Int::is_negative(self)`): a call on a `&FixedInt` receiver — as in the
83    /// `fixed_impl!` macro's `sign_of` — then binds the `Signed` *trait* method
84    /// instead, keeping the macro's `use num_traits::Signed` genuinely used,
85    /// while calls on an owned value (`lambda1024`) bind this inherent one.
86    #[inline]
87    pub fn is_negative(self) -> bool {
88        self.0[K - 1] >> 63 == 1
89    }
90}
91
92// ── Native limb helpers (the hot path) ──────────────────────────────────────
93
94#[inline]
95fn is_neg<const K: usize>(a: &[u64; K]) -> bool {
96    a[K - 1] >> 63 == 1
97}
98
99/// Two's-complement negation of the limb array (`!a + 1`). Wraps on `MIN`
100/// exactly like `bnum`'s `wrapping_neg`.
101#[inline]
102fn negate<const K: usize>(a: &[u64; K]) -> [u64; K] {
103    let mut out = [0u64; K];
104    let mut carry = 1u64;
105    for i in 0..K {
106        let (v, c) = (!a[i]).overflowing_add(carry);
107        out[i] = v;
108        carry = c as u64;
109    }
110    out
111}
112
113/// Bit length of an UNSIGNED magnitude limb array (0 for all-zero).
114#[inline]
115fn bit_length<const K: usize>(a: &[u64; K]) -> usize {
116    for i in (0..K).rev() {
117        if a[i] != 0 {
118            return i * 64 + (64 - a[i].leading_zeros() as usize);
119        }
120    }
121    0
122}
123
124/// `(|a|, a<0)`. For `a == MIN`, `negate` returns the `MIN` bit pattern which
125/// reads as the correct unsigned magnitude `2^(K*64-1)`.
126#[inline]
127fn magnitude<const K: usize>(a: &FixedInt<K>) -> ([u64; K], bool) {
128    if is_neg(&a.0) {
129        (negate(&a.0), true)
130    } else {
131        (a.0, false)
132    }
133}
134
135
136/// Wrapping two's-complement limb addition (drops the final carry). Kept a free
137/// function so the carry-combine `|` stays out of the `Add` trait impl, where
138/// clippy's `suspicious_arithmetic_impl` would (wrongly) flag it.
139#[inline]
140fn wrapping_add<const K: usize>(a: &[u64; K], b: &[u64; K]) -> [u64; K] {
141    let mut out = [0u64; K];
142    let mut carry = 0u64;
143    for i in 0..K {
144        let (s1, c1) = a[i].overflowing_add(b[i]);
145        let (s2, c2) = s1.overflowing_add(carry);
146        out[i] = s2;
147        // At most one of c1, c2 can be set, so `|` == the true 0/1 carry.
148        carry = (c1 as u64) | (c2 as u64);
149    }
150    out
151}
152
153/// Wrapping two's-complement limb subtraction (drops the final borrow).
154#[inline]
155fn wrapping_sub<const K: usize>(a: &[u64; K], b: &[u64; K]) -> [u64; K] {
156    let mut out = [0u64; K];
157    let mut borrow = 0u64;
158    for i in 0..K {
159        let (d1, b1) = a[i].overflowing_sub(b[i]);
160        let (d2, b2) = d1.overflowing_sub(borrow);
161        out[i] = d2;
162        borrow = (b1 as u64) | (b2 as u64);
163    }
164    out
165}
166
167/// Native two's-complement addition with signed-overflow detection.
168#[inline]
169fn checked_add_limbs<const K: usize>(a: &FixedInt<K>, b: &FixedInt<K>) -> Option<FixedInt<K>> {
170    let out = wrapping_add(&a.0, &b.0);
171    let sa = a.0[K - 1] >> 63;
172    let sb = b.0[K - 1] >> 63;
173    let sr = out[K - 1] >> 63;
174    // Overflow iff the operands share a sign and the result flips it.
175    if sa == sb && sr != sa {
176        None
177    } else {
178        Some(FixedInt(out))
179    }
180}
181
182/// Native two's-complement subtraction with signed-overflow detection.
183#[inline]
184fn checked_sub_limbs<const K: usize>(a: &FixedInt<K>, b: &FixedInt<K>) -> Option<FixedInt<K>> {
185    let out = wrapping_sub(&a.0, &b.0);
186    let sa = a.0[K - 1] >> 63;
187    let sb = b.0[K - 1] >> 63;
188    let sr = out[K - 1] >> 63;
189    // Overflow iff the operands differ in sign and the result flips the minuend.
190    if sa != sb && sr != sa {
191        None
192    } else {
193        Some(FixedInt(out))
194    }
195}
196
197/// Native checked multiply with a CONSERVATIVE bit-length precheck.
198///
199/// The signed range of a K-limb value is `[-2^(w-1), 2^(w-1)-1]` with
200/// `w = K*64`. Let `la = bitlen(|a|)`, `lb = bitlen(|b|)`.
201/// - `la + lb <= w-1` ⇒ `|a*b| < 2^(w-1)` ⇒ fits both signs ⇒ fast low-K product.
202/// - `la + lb >= w+2` ⇒ `|a*b| >= 2^w > 2^(w-1)` ⇒ overflows both signs ⇒ `None`.
203/// - `la + lb ∈ {w, w+1}` (ambiguous, straddles `MIN`): compute the full 2K-limb
204///   two's-complement product and narrow it exactly (fits iff the high K limbs
205///   are the sign extension of limb `K-1`, which correctly admits exactly `MIN`).
206///
207/// Both prechecks are one-sided/conservative, so a wrong `Some` is impossible;
208/// the differential fuzz pins agreement with `bnum` on value and verdict.
209#[inline]
210fn checked_mul_limbs<const K: usize>(a: &FixedInt<K>, b: &FixedInt<K>) -> Option<FixedInt<K>> {
211    let (ma, sa) = magnitude(a);
212    let (mb, sb) = magnitude(b);
213    let la = bit_length(&ma);
214    let lb = bit_length(&mb);
215    if la == 0 || lb == 0 {
216        return Some(FixedInt([0u64; K]));
217    }
218    let neg = sa ^ sb;
219    let w = K * 64;
220
221    if la + lb <= w - 1 {
222        // Fast-accept: provably fits.
223        let lo = mul_low(&ma, &mb);
224        let out = if neg { negate(&lo) } else { lo };
225        return Some(FixedInt(out));
226    }
227    if la + lb >= w + 2 {
228        // Fast-reject: provably overflows.
229        return None;
230    }
231
232    // Ambiguous band: full 2K-limb magnitude product, then exact narrowing.
233    debug_assert!(K <= 32, "FixedInt checked_mul scratch supports only K <= 32");
234    let n2 = 2 * K;
235    let mut full = [0u64; 64];
236    mul_full(&ma, &mb, &mut full);
237    if neg {
238        // Two's-complement negate over the low n2 limbs in place.
239        let mut c = 1u64;
240        for slot in full.iter_mut().take(n2) {
241            let (v, cc) = (!*slot).overflowing_add(c);
242            *slot = v;
243            c = cc as u64;
244        }
245    }
246    // Fits in K limbs iff the upper half is the sign extension of the low half.
247    let ext = if full[K - 1] >> 63 == 1 { u64::MAX } else { 0 };
248    for &limb in &full[K..n2] {
249        if limb != ext {
250            return None;
251        }
252    }
253    let mut out = [0u64; K];
254    out.copy_from_slice(&full[..K]);
255    Some(FixedInt(out))
256}
257
258// ── bnum byte-bridge (the cold path) ────────────────────────────────────────
259
260/// Parse error placeholder for [`Num::from_str_radix`]; the predicate cascade
261/// never calls it, the impl exists only to satisfy the `Num` trait bound.
262#[derive(Debug, PartialEq, Eq)]
263pub struct ParseFixedIntError;
264
265/// Serialize the limbs into a fixed 256-byte little-endian scratch buffer
266/// (32 limbs = the widest supported width, I2048). Only the first `K*8` bytes
267/// are meaningful.
268#[inline]
269fn to_le_scratch<const K: usize>(x: &FixedInt<K>) -> [u8; 256] {
270    debug_assert!(K <= 32, "FixedInt bnum bridge supports only K <= 32");
271    let mut buf = [0u8; 256];
272    for i in 0..K {
273        buf[i * 8..i * 8 + 8].copy_from_slice(&x.0[i].to_le_bytes());
274    }
275    buf
276}
277
278/// Rebuild a `FixedInt<K>` from the first `K*8` little-endian bytes of `buf`.
279#[inline]
280fn from_le_scratch<const K: usize>(buf: &[u8]) -> FixedInt<K> {
281    let mut limbs = [0u64; K];
282    for i in 0..K {
283        let mut b = [0u8; 8];
284        b.copy_from_slice(&buf[i * 8..i * 8 + 8]);
285        limbs[i] = u64::from_le_bytes(b);
286    }
287    FixedInt(limbs)
288}
289
290/// Dispatch a binary op on the `bnum` signed type matching the width `K`,
291/// round-tripping both operands and the result through little-endian bytes.
292/// The `from_le_slice`/`to_le_bytes` layout is bit-identical to `FixedInt`, so
293/// the round-trip is lossless; the `unwrap` cannot fail because every canonical
294/// `K*8`-byte value is representable in `I{K*64}` (byte-aligned width, no pad).
295macro_rules! bnum_binary {
296    ($K:expr, $ab:expr, $bb:expr, |$x:ident, $y:ident| $op:expr) => {{
297        let nb = $K * 8;
298        match $K {
299            4 => {
300                let $x = bnum::types::I256::from_le_slice(&$ab[..nb]).unwrap();
301                let $y = bnum::types::I256::from_le_slice(&$bb[..nb]).unwrap();
302                from_le_scratch::<$K>(&($op).to_le_bytes())
303            }
304            8 => {
305                let $x = bnum::types::I512::from_le_slice(&$ab[..nb]).unwrap();
306                let $y = bnum::types::I512::from_le_slice(&$bb[..nb]).unwrap();
307                from_le_scratch::<$K>(&($op).to_le_bytes())
308            }
309            16 => {
310                let $x = bnum::types::I1024::from_le_slice(&$ab[..nb]).unwrap();
311                let $y = bnum::types::I1024::from_le_slice(&$bb[..nb]).unwrap();
312                from_le_scratch::<$K>(&($op).to_le_bytes())
313            }
314            32 => {
315                let $x = bnum::types::I2048::from_le_slice(&$ab[..nb]).unwrap();
316                let $y = bnum::types::I2048::from_le_slice(&$bb[..nb]).unwrap();
317                from_le_scratch::<$K>(&($op).to_le_bytes())
318            }
319            _ => panic!("FixedInt<{}>: bnum bridge supports only K in {{4,8,16,32}}", $K),
320        }
321    }};
322}
323
324#[inline]
325fn bnum_to_f64<const K: usize>(buf: &[u8]) -> Option<f64> {
326    let nb = K * 8;
327    match K {
328        4 => bnum::types::I256::from_le_slice(&buf[..nb]).unwrap().to_f64(),
329        8 => bnum::types::I512::from_le_slice(&buf[..nb]).unwrap().to_f64(),
330        16 => bnum::types::I1024::from_le_slice(&buf[..nb]).unwrap().to_f64(),
331        32 => bnum::types::I2048::from_le_slice(&buf[..nb]).unwrap().to_f64(),
332        _ => panic!("FixedInt<{K}>: bnum bridge supports only K in {{4,8,16,32}}"),
333    }
334}
335
336#[inline]
337fn bnum_from_str_radix<const K: usize>(
338    s: &str,
339    radix: u32,
340) -> Result<FixedInt<K>, ParseFixedIntError> {
341    match K {
342        4 => bnum::types::I256::from_str_radix(s, radix)
343            .map(|v| from_le_scratch::<K>(&v.to_le_bytes()))
344            .map_err(|_| ParseFixedIntError),
345        8 => bnum::types::I512::from_str_radix(s, radix)
346            .map(|v| from_le_scratch::<K>(&v.to_le_bytes()))
347            .map_err(|_| ParseFixedIntError),
348        16 => bnum::types::I1024::from_str_radix(s, radix)
349            .map(|v| from_le_scratch::<K>(&v.to_le_bytes()))
350            .map_err(|_| ParseFixedIntError),
351        32 => bnum::types::I2048::from_str_radix(s, radix)
352            .map(|v| from_le_scratch::<K>(&v.to_le_bytes()))
353            .map_err(|_| ParseFixedIntError),
354        _ => panic!("FixedInt<{K}>: bnum bridge supports only K in {{4,8,16,32}}"),
355    }
356}
357
358// ── std::ops ────────────────────────────────────────────────────────────────
359// Bare `Add`/`Sub`/`Mul` exist to satisfy the `CheckedAdd`/`CheckedSub`/
360// `CheckedMul`/`Num`/`One` supertrait bounds; the macro never calls them hot.
361// They wrap on overflow (two's complement), matching `bnum`'s `wrapping_*`.
362
363impl<const K: usize> Add for FixedInt<K> {
364    type Output = Self;
365    #[inline]
366    fn add(self, rhs: Self) -> Self {
367        FixedInt(wrapping_add(&self.0, &rhs.0))
368    }
369}
370
371impl<const K: usize> Sub for FixedInt<K> {
372    type Output = Self;
373    #[inline]
374    fn sub(self, rhs: Self) -> Self {
375        FixedInt(wrapping_sub(&self.0, &rhs.0))
376    }
377}
378
379impl<const K: usize> Mul for FixedInt<K> {
380    type Output = Self;
381    #[inline]
382    fn mul(self, rhs: Self) -> Self {
383        // Low-K limbs of the product agree for the signed and unsigned
384        // interpretations, so this is the wrapping signed product.
385        FixedInt(mul_low(&self.0, &rhs.0))
386    }
387}
388
389impl<const K: usize> Neg for FixedInt<K> {
390    type Output = Self;
391    #[inline]
392    fn neg(self) -> Self {
393        FixedInt(negate(&self.0))
394    }
395}
396
397impl<const K: usize> Div for FixedInt<K> {
398    type Output = Self;
399    #[inline]
400    fn div(self, rhs: Self) -> Self {
401        let ab = to_le_scratch(&self);
402        let bb = to_le_scratch(&rhs);
403        bnum_binary!(K, ab, bb, |x, y| x / y)
404    }
405}
406
407impl<const K: usize> Rem for FixedInt<K> {
408    type Output = Self;
409    #[inline]
410    fn rem(self, rhs: Self) -> Self {
411        let ab = to_le_scratch(&self);
412        let bb = to_le_scratch(&rhs);
413        bnum_binary!(K, ab, bb, |x, y| x % y)
414    }
415}
416
417// ── num_traits ──────────────────────────────────────────────────────────────
418
419impl<const K: usize> Zero for FixedInt<K> {
420    #[inline]
421    fn zero() -> Self {
422        FixedInt([0u64; K])
423    }
424    #[inline]
425    fn is_zero(&self) -> bool {
426        self.0.iter().all(|&l| l == 0)
427    }
428}
429
430impl<const K: usize> One for FixedInt<K> {
431    #[inline]
432    fn one() -> Self {
433        let mut limbs = [0u64; K];
434        limbs[0] = 1;
435        FixedInt(limbs)
436    }
437    #[inline]
438    fn is_one(&self) -> bool {
439        self.0[0] == 1 && self.0[1..].iter().all(|&l| l == 0)
440    }
441}
442
443impl<const K: usize> Num for FixedInt<K> {
444    type FromStrRadixErr = ParseFixedIntError;
445    #[inline]
446    fn from_str_radix(s: &str, radix: u32) -> Result<Self, Self::FromStrRadixErr> {
447        bnum_from_str_radix::<K>(s, radix)
448    }
449}
450
451impl<const K: usize> Signed for FixedInt<K> {
452    #[inline]
453    fn abs(&self) -> Self {
454        if is_neg(&self.0) {
455            FixedInt(negate(&self.0))
456        } else {
457            *self
458        }
459    }
460    #[inline]
461    fn abs_sub(&self, other: &Self) -> Self {
462        if self <= other {
463            FixedInt([0u64; K])
464        } else {
465            *self - *other
466        }
467    }
468    #[inline]
469    fn signum(&self) -> Self {
470        if is_neg(&self.0) {
471            FixedInt([u64::MAX; K]) // -1
472        } else if self.is_zero() {
473            FixedInt([0u64; K])
474        } else {
475            <Self as One>::one()
476        }
477    }
478    #[inline]
479    fn is_positive(&self) -> bool {
480        !is_neg(&self.0) && !self.is_zero()
481    }
482    #[inline]
483    fn is_negative(&self) -> bool {
484        is_neg(&self.0)
485    }
486}
487
488impl<const K: usize> FromPrimitive for FixedInt<K> {
489    #[inline]
490    fn from_i64(n: i64) -> Option<Self> {
491        let mut limbs = if n < 0 { [u64::MAX; K] } else { [0u64; K] };
492        limbs[0] = n as u64;
493        Some(FixedInt(limbs))
494    }
495    #[inline]
496    fn from_u64(n: u64) -> Option<Self> {
497        let mut limbs = [0u64; K];
498        limbs[0] = n;
499        Some(FixedInt(limbs))
500    }
501}
502
503impl<const K: usize> ToPrimitive for FixedInt<K> {
504    #[inline]
505    fn to_i64(&self) -> Option<i64> {
506        if is_neg(&self.0) {
507            for i in 1..K {
508                if self.0[i] != u64::MAX {
509                    return None;
510                }
511            }
512            let v = self.0[0];
513            if v >> 63 == 1 {
514                Some(v as i64)
515            } else {
516                None
517            }
518        } else {
519            for i in 1..K {
520                if self.0[i] != 0 {
521                    return None;
522                }
523            }
524            let v = self.0[0];
525            if v >> 63 == 0 {
526                Some(v as i64)
527            } else {
528                None
529            }
530        }
531    }
532    #[inline]
533    fn to_u64(&self) -> Option<u64> {
534        if is_neg(&self.0) {
535            return None;
536        }
537        for i in 1..K {
538            if self.0[i] != 0 {
539                return None;
540            }
541        }
542        Some(self.0[0])
543    }
544    #[inline]
545    fn to_f64(&self) -> Option<f64> {
546        // Delegate to bnum: the default `to_f64` routes through `to_i64` and
547        // would return `None` for the wide lambda values `point_to_f64` feeds it.
548        let buf = to_le_scratch(self);
549        bnum_to_f64::<K>(&buf)
550    }
551}
552
553impl<const K: usize> CheckedAdd for FixedInt<K> {
554    #[inline]
555    fn checked_add(&self, v: &Self) -> Option<Self> {
556        checked_add_limbs(self, v)
557    }
558}
559
560impl<const K: usize> CheckedSub for FixedInt<K> {
561    #[inline]
562    fn checked_sub(&self, v: &Self) -> Option<Self> {
563        checked_sub_limbs(self, v)
564    }
565}
566
567impl<const K: usize> CheckedMul for FixedInt<K> {
568    #[inline]
569    fn checked_mul(&self, v: &Self) -> Option<Self> {
570        checked_mul_limbs(self, v)
571    }
572}
573
574impl<const K: usize> Ord for FixedInt<K> {
575    #[inline]
576    fn cmp(&self, other: &Self) -> Ordering {
577        match (is_neg(&self.0), is_neg(&other.0)) {
578            (true, false) => Ordering::Less,
579            (false, true) => Ordering::Greater,
580            // Same sign: unsigned high-to-low limb compare gives signed order.
581            _ => {
582                for i in (0..K).rev() {
583                    match self.0[i].cmp(&other.0[i]) {
584                        Ordering::Equal => {}
585                        o => return o,
586                    }
587                }
588                Ordering::Equal
589            }
590        }
591    }
592}
593
594impl<const K: usize> PartialOrd for FixedInt<K> {
595    #[inline]
596    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
597        Some(self.cmp(other))
598    }
599}
600
601// The differential fuzz vs bnum (the correctness oracle) lives in the sibling
602// `tests.rs`; it is a child module so it keeps access to the private limbs.
603#[cfg(test)]
604mod tests;