Skip to main content

sema_core/
number.rs

1//! The Sema numeric tower: exact integers (arbitrary precision), exact
2//! rationals, inexact reals, and complex numbers. This module is the
3//! arithmetic currency — `Value` lifts operands into `SemaNumber`, computes
4//! here, and lowers the result back to the tightest `Value` representation.
5//! It has NO dependency on NaN-boxing and is unit-tested in isolation.
6
7use num_bigint::BigInt;
8use num_rational::BigRational;
9
10/// A number anywhere in the tower. Invariants (upheld by every constructor
11/// and arithmetic op via `normalize`):
12/// - `Rational` is reduced and its denominator is > 1 (denom == 1 ⇒ `Integer`).
13/// - `Complex`'s imaginary part is never an exact zero (⇒ the real part alone).
14/// - `Complex` components are themselves never `Complex`.
15#[derive(Clone, Debug)]
16pub enum SemaNumber {
17    Integer(BigInt),
18    Rational(BigRational),
19    Real(f64),
20    Complex(Box<Complex>),
21}
22
23/// A non-real number `re + im·i`. Components are `Integer`, `Rational`, or
24/// `Real` — never `Complex`. Exactness is per-component (a complex is exact
25/// iff both components are exact).
26#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
27pub struct Complex {
28    pub re: SemaNumber,
29    pub im: SemaNumber,
30}
31
32impl SemaNumber {
33    /// True unless any component is an inexact `Real`.
34    pub fn is_exact(&self) -> bool {
35        match self {
36            SemaNumber::Integer(_) | SemaNumber::Rational(_) => true,
37            SemaNumber::Real(_) => false,
38            SemaNumber::Complex(c) => c.re.is_exact() && c.im.is_exact(),
39        }
40    }
41
42    /// True for `Integer` and for any real-valued number equal to an integer.
43    /// (A `Real` like `2.0` is an integer in the R7RS `integer?` sense.)
44    pub fn is_integer(&self) -> bool {
45        match self {
46            SemaNumber::Integer(_) => true,
47            SemaNumber::Rational(_) => false,
48            SemaNumber::Real(f) => f.is_finite() && f.fract() == 0.0,
49            SemaNumber::Complex(_) => false,
50        }
51    }
52
53    /// True for everything except `Complex`.
54    pub fn is_real(&self) -> bool {
55        !matches!(self, SemaNumber::Complex(_))
56    }
57
58    /// Collapse to the tightest canonical form (see the type invariants).
59    /// Cheap and idempotent; every lowering constructor and arithmetic result
60    /// passes through it.
61    pub fn normalize(self) -> SemaNumber {
62        use num_traits::{One, Zero};
63        match self {
64            SemaNumber::Rational(r) => {
65                if r.denom().is_one() {
66                    SemaNumber::Integer(r.numer().clone())
67                } else {
68                    SemaNumber::Rational(r)
69                }
70            }
71            SemaNumber::Complex(c) => {
72                let re = c.re.normalize();
73                let im = c.im.normalize();
74                // Exact zero imaginary part ⇒ a real number. An inexact 0.0
75                // must be preserved (the value is still non-real per R7RS).
76                let im_is_exact_zero = matches!(&im, SemaNumber::Integer(n) if n.is_zero());
77                if im_is_exact_zero {
78                    re
79                } else {
80                    SemaNumber::Complex(Box::new(Complex { re, im }))
81                }
82            }
83            other => other,
84        }
85    }
86
87    /// Lossy projection to `f64` for inexact operations (`sqrt`, `sin`, mixed
88    /// arithmetic). A `Complex` cannot project to a real — returns `f64::NAN`;
89    /// callers that can receive complex must special-case it before calling.
90    pub fn to_f64(&self) -> f64 {
91        use num_traits::ToPrimitive;
92        match self {
93            SemaNumber::Integer(n) => n.to_f64().unwrap_or(f64::INFINITY),
94            SemaNumber::Rational(r) => r.to_f64().unwrap_or(f64::INFINITY),
95            SemaNumber::Real(f) => *f,
96            SemaNumber::Complex(_) => f64::NAN,
97        }
98    }
99
100    /// Tower level for promotion ordering.
101    fn level(&self) -> u8 {
102        match self {
103            SemaNumber::Integer(_) => 0,
104            SemaNumber::Rational(_) => 1,
105            SemaNumber::Real(_) => 2,
106            SemaNumber::Complex(_) => 3,
107        }
108    }
109
110    /// Lift `self` up to the given level (never down). `Integer→Rational` is
111    /// exact; `→Real` uses `to_f64`; `→Complex` pairs with an exact 0
112    /// imaginary part.
113    fn lift_to(self, level: u8) -> SemaNumber {
114        use num_traits::Zero;
115        match (self.level(), level) {
116            (a, b) if a >= b => self,
117            (0, 1) => match self {
118                SemaNumber::Integer(n) => SemaNumber::Rational(BigRational::from(n)),
119                _ => unreachable!(),
120            },
121            (_, 2) => SemaNumber::Real(self.to_f64()),
122            (_, 3) => SemaNumber::Complex(Box::new(Complex {
123                re: self,
124                im: SemaNumber::Integer(BigInt::zero()),
125            })),
126            // (0,1) handled; (1,2)/(2,3) handled by the level==2/3 arms above.
127            _ => self,
128        }
129    }
130
131    /// Lift both operands to `max(level(a), level(b))` so a binary op has a
132    /// single same-level case to implement per level.
133    pub fn promote(a: SemaNumber, b: SemaNumber) -> (SemaNumber, SemaNumber) {
134        let target = a.level().max(b.level());
135        (a.lift_to(target), b.lift_to(target))
136    }
137
138    // `add`/`sub`/`mul`/`div`/`neg` deliberately mirror the `std::ops` method
139    // names (this is the tower's public arithmetic interface, consumed by
140    // later phases as `SemaNumber::add` etc.) rather than implementing the
141    // traits, since `div` must return `Result` for divide-by-zero signalling.
142    #[allow(clippy::should_implement_trait)]
143    pub fn neg(self) -> SemaNumber {
144        match self {
145            SemaNumber::Integer(n) => SemaNumber::Integer(-n),
146            SemaNumber::Rational(r) => SemaNumber::Rational(-r),
147            SemaNumber::Real(f) => SemaNumber::Real(-f),
148            SemaNumber::Complex(c) => SemaNumber::Complex(Box::new(Complex {
149                re: c.re.neg(),
150                im: c.im.neg(),
151            })),
152        }
153        .normalize()
154    }
155
156    /// The exact square root of a non-negative exact perfect square, else
157    /// `None`. Keeps `(sqrt 4) => 2` exact and lets `(sqrt -1) => +i` produce
158    /// an exact imaginary part (R7RS). Inexact reals and non-squares yield
159    /// `None`, leaving the caller to fall back to `f64::sqrt`.
160    pub fn exact_sqrt(&self) -> Option<SemaNumber> {
161        use num_traits::Signed;
162        match self {
163            SemaNumber::Integer(n) if !n.is_negative() => {
164                let root = n.sqrt();
165                (&root * &root == *n).then_some(SemaNumber::Integer(root))
166            }
167            SemaNumber::Rational(r) if !r.is_negative() => {
168                // A reduced non-negative rational is a perfect square iff its
169                // numerator and denominator are both perfect squares.
170                let (rn, rd) = (r.numer().sqrt(), r.denom().sqrt());
171                (&rn * &rn == *r.numer() && &rd * &rd == *r.denom())
172                    .then(|| SemaNumber::Rational(BigRational::new(rn, rd)).normalize())
173            }
174            _ => None,
175        }
176    }
177
178    #[allow(clippy::should_implement_trait)]
179    pub fn add(self, other: SemaNumber) -> SemaNumber {
180        let (a, b) = SemaNumber::promote(self, other);
181        match (a, b) {
182            (SemaNumber::Integer(x), SemaNumber::Integer(y)) => SemaNumber::Integer(x + y),
183            (SemaNumber::Rational(x), SemaNumber::Rational(y)) => SemaNumber::Rational(x + y),
184            (SemaNumber::Real(x), SemaNumber::Real(y)) => SemaNumber::Real(x + y),
185            (SemaNumber::Complex(x), SemaNumber::Complex(y)) => {
186                SemaNumber::Complex(Box::new(Complex {
187                    re: x.re.add(y.re),
188                    im: x.im.add(y.im),
189                }))
190            }
191            _ => unreachable!("promote guarantees equal levels"),
192        }
193        .normalize()
194    }
195
196    #[allow(clippy::should_implement_trait)]
197    pub fn sub(self, other: SemaNumber) -> SemaNumber {
198        self.add(other.neg())
199    }
200
201    /// Non-negative magnitude. For a real number this is the usual absolute
202    /// value (exactness-preserving: an exact input stays exact). For a
203    /// `Complex`, this is its `f64` hypot — always inexact, since the tower
204    /// has no exact square root in general.
205    pub fn abs(self) -> SemaNumber {
206        use num_traits::Signed;
207        match self {
208            SemaNumber::Integer(n) => SemaNumber::Integer(n.abs()),
209            SemaNumber::Rational(r) => SemaNumber::Rational(r.abs()),
210            SemaNumber::Real(f) => SemaNumber::Real(f.abs()),
211            SemaNumber::Complex(c) => SemaNumber::Real(c.re.to_f64().hypot(c.im.to_f64())),
212        }
213        .normalize()
214    }
215
216    /// Round toward negative infinity. Exactness-preserving: an exact
217    /// rational rounds to an exact `Integer`; an inexact `Real` stays a
218    /// `Real`. Not meaningful for `Complex` — callers must guard with
219    /// `is_real()` before calling (mirrors `cmp_real`'s contract).
220    pub fn floor(self) -> SemaNumber {
221        match self {
222            SemaNumber::Integer(n) => SemaNumber::Integer(n),
223            SemaNumber::Rational(r) => SemaNumber::Integer(r.floor().to_integer()),
224            SemaNumber::Real(f) => SemaNumber::Real(f.floor()),
225            SemaNumber::Complex(_) => unreachable!("caller must guard complex via is_real()"),
226        }
227    }
228
229    /// Round toward positive infinity. See `floor` for exactness rules.
230    pub fn ceil(self) -> SemaNumber {
231        match self {
232            SemaNumber::Integer(n) => SemaNumber::Integer(n),
233            SemaNumber::Rational(r) => SemaNumber::Integer(r.ceil().to_integer()),
234            SemaNumber::Real(f) => SemaNumber::Real(f.ceil()),
235            SemaNumber::Complex(_) => unreachable!("caller must guard complex via is_real()"),
236        }
237    }
238
239    /// Round toward zero (truncate the fractional part). See `floor` for
240    /// exactness rules.
241    pub fn truncate(self) -> SemaNumber {
242        match self {
243            SemaNumber::Integer(n) => SemaNumber::Integer(n),
244            SemaNumber::Rational(r) => SemaNumber::Integer(r.trunc().to_integer()),
245            SemaNumber::Real(f) => SemaNumber::Real(f.trunc()),
246            SemaNumber::Complex(_) => unreachable!("caller must guard complex via is_real()"),
247        }
248    }
249
250    /// Round to the nearest integer, ties to even (R7RS "banker's rounding").
251    /// See `floor` for exactness rules.
252    pub fn round(self) -> SemaNumber {
253        use num_integer::Integer;
254        use std::cmp::Ordering;
255        match self {
256            SemaNumber::Integer(n) => SemaNumber::Integer(n),
257            SemaNumber::Rational(r) => {
258                let (numer, denom) = (r.numer().clone(), r.denom().clone());
259                // denom is always positive (BigRational's invariant), so
260                // div_floor/rem here behave like ordinary floored division.
261                let floor = numer.div_floor(&denom);
262                let rem = &numer - &floor * &denom; // in [0, denom)
263                let twice_rem = &rem * BigInt::from(2);
264                let round_up = match twice_rem.cmp(&denom) {
265                    Ordering::Less => false,
266                    Ordering::Greater => true,
267                    // Exact tie: round to whichever neighbor is even.
268                    Ordering::Equal => floor.is_odd(),
269                };
270                let result = if round_up { floor + 1 } else { floor };
271                SemaNumber::Integer(result)
272            }
273            SemaNumber::Real(f) => SemaNumber::Real(f.round_ties_even()),
274            SemaNumber::Complex(_) => unreachable!("caller must guard complex via is_real()"),
275        }
276    }
277
278    #[allow(clippy::should_implement_trait)]
279    pub fn mul(self, other: SemaNumber) -> SemaNumber {
280        let (a, b) = SemaNumber::promote(self, other);
281        match (a, b) {
282            (SemaNumber::Integer(x), SemaNumber::Integer(y)) => SemaNumber::Integer(x * y),
283            (SemaNumber::Rational(x), SemaNumber::Rational(y)) => SemaNumber::Rational(x * y),
284            (SemaNumber::Real(x), SemaNumber::Real(y)) => SemaNumber::Real(x * y),
285            (SemaNumber::Complex(x), SemaNumber::Complex(y)) => {
286                // (a+bi)(c+di) = (ac - bd) + (ad + bc)i
287                let ac = x.re.clone().mul(y.re.clone());
288                let bd = x.im.clone().mul(y.im.clone());
289                let ad = x.re.mul(y.im.clone());
290                let bc = x.im.mul(y.re);
291                SemaNumber::Complex(Box::new(Complex {
292                    re: ac.sub(bd),
293                    im: ad.add(bc),
294                }))
295            }
296            _ => unreachable!("promote guarantees equal levels"),
297        }
298        .normalize()
299    }
300}
301
302/// Returned by `SemaNumber::div` when dividing by an *exact* zero. An inexact
303/// zero divisor follows IEEE-754 (→ ±inf / NaN), matching Scheme.
304#[derive(Debug, Clone, Copy, PartialEq, Eq)]
305pub struct DivByZero;
306
307impl SemaNumber {
308    #[allow(clippy::should_implement_trait)]
309    pub fn div(self, other: SemaNumber) -> Result<SemaNumber, DivByZero> {
310        use num_traits::Zero;
311        // Guard exact-zero divisor up front (before promotion, so `1/0` and
312        // `(1/2)/0` both signal, but `1/0.0` falls through to IEEE).
313        if matches!(&other, SemaNumber::Integer(n) if n.is_zero())
314            || matches!(&other, SemaNumber::Rational(r) if r.numer().is_zero())
315        {
316            return Err(DivByZero);
317        }
318        let (a, b) = SemaNumber::promote(self, other);
319        let out = match (a, b) {
320            // Integer/Integer → exact rational (reduces; normalize collapses to Integer if whole).
321            (SemaNumber::Integer(x), SemaNumber::Integer(y)) => {
322                SemaNumber::Rational(BigRational::new(x, y))
323            }
324            (SemaNumber::Rational(x), SemaNumber::Rational(y)) => SemaNumber::Rational(x / y),
325            (SemaNumber::Real(x), SemaNumber::Real(y)) => SemaNumber::Real(x / y),
326            (SemaNumber::Complex(x), SemaNumber::Complex(y)) => {
327                // (a+bi)/(c+di) = ((a+bi)(c-di)) / (c²+d²)
328                let denom =
329                    y.re.clone()
330                        .mul(y.re.clone())
331                        .add(y.im.clone().mul(y.im.clone()));
332                let num = SemaNumber::Complex(x).mul(SemaNumber::Complex(Box::new(Complex {
333                    re: y.re,
334                    im: y.im.neg(),
335                })));
336                match num {
337                    SemaNumber::Complex(nc) => SemaNumber::Complex(Box::new(Complex {
338                        re: nc.re.div(denom.clone())?,
339                        im: nc.im.div(denom)?,
340                    })),
341                    // num collapsed to real (imaginary cancelled): divide directly.
342                    real => real.div(denom)?,
343                }
344            }
345            _ => unreachable!("promote guarantees equal levels"),
346        };
347        Ok(out.normalize())
348    }
349
350    /// Raise `self` to an arbitrary-precision integer exponent via repeated
351    /// squaring — O(log |exp|) multiplications, so `(expt 2 100)` costs ~7
352    /// squarings rather than 100. A negative exponent yields the reciprocal
353    /// (an exact base stays exact: `2^-3 => 1/8`); base 0 with a negative
354    /// exponent divides by zero. Only meaningful for exact/inexact reals —
355    /// callers pick this path themselves (float exponents fall back to
356    /// `f64::powf` at the builtin layer).
357    pub fn powi(self, exp: &BigInt) -> Result<SemaNumber, DivByZero> {
358        use num_integer::Integer;
359        use num_traits::{Signed, Zero};
360        let negative = exp.is_negative();
361        let mut e = if negative { -exp } else { exp.clone() };
362        let mut result = SemaNumber::from_i64(1);
363        let mut base = self;
364        while !e.is_zero() {
365            if e.is_odd() {
366                result = result.mul(base.clone());
367            }
368            e = e.div_floor(&BigInt::from(2));
369            if !e.is_zero() {
370                base = base.clone().mul(base);
371            }
372        }
373        if negative {
374            SemaNumber::from_i64(1).div(result)
375        } else {
376            Ok(result)
377        }
378    }
379}
380
381impl SemaNumber {
382    /// Convert a finite `f64` to its exact rational value (no rounding). Used
383    /// so exact-vs-inexact comparison never loses precision above 2^53.
384    fn real_to_exact(f: f64) -> Option<SemaNumber> {
385        if !f.is_finite() {
386            return None;
387        }
388        // BigRational::from_float is exact for finite inputs.
389        num_rational::BigRational::from_float(f).map(SemaNumber::Rational)
390    }
391
392    pub fn num_eq(&self, other: &SemaNumber) -> bool {
393        match (self, other) {
394            (SemaNumber::Complex(a), SemaNumber::Complex(b)) => {
395                a.re.num_eq(&b.re) && a.im.num_eq(&b.im)
396            }
397            (SemaNumber::Complex(_), _) | (_, SemaNumber::Complex(_)) => false,
398            _ => self.cmp_real(other) == Some(std::cmp::Ordering::Equal),
399        }
400    }
401
402    /// Ordering for real numbers. `None` if either operand is complex or a NaN.
403    /// Exact-vs-inexact converts the float to an exact rational so the compare
404    /// is precise even above 2^53.
405    pub fn cmp_real(&self, other: &SemaNumber) -> Option<std::cmp::Ordering> {
406        use std::cmp::Ordering;
407        if matches!(self, SemaNumber::Complex(_)) || matches!(other, SemaNumber::Complex(_)) {
408            return None;
409        }
410        // If both inexact, compare as f64 (preserves NaN → None).
411        if let (SemaNumber::Real(x), SemaNumber::Real(y)) = (self, other) {
412            return x.partial_cmp(y);
413        }
414        // Fast path for the infinity/NaN cases: if exactly one side is a
415        // non-finite Real, its sign decides.
416        match (self, other) {
417            (SemaNumber::Real(f), _) if !f.is_finite() => {
418                return if f.is_nan() {
419                    None
420                } else if *f > 0.0 {
421                    Some(Ordering::Greater)
422                } else {
423                    Some(Ordering::Less)
424                };
425            }
426            (_, SemaNumber::Real(f)) if !f.is_finite() => {
427                return if f.is_nan() {
428                    None
429                } else if *f > 0.0 {
430                    Some(Ordering::Less)
431                } else {
432                    Some(Ordering::Greater)
433                };
434            }
435            _ => {}
436        }
437        // Mixed or both-exact: lift any (finite) Real to an exact rational.
438        let to_exact = |v: &SemaNumber| -> SemaNumber {
439            match v {
440                SemaNumber::Real(f) => {
441                    SemaNumber::real_to_exact(*f).expect("finite (checked above)")
442                }
443                other => other.clone(),
444            }
445        };
446        let a = to_exact(self);
447        let b = to_exact(other);
448        let (a, b) = SemaNumber::promote(a, b);
449        match (a, b) {
450            (SemaNumber::Integer(x), SemaNumber::Integer(y)) => Some(x.cmp(&y)),
451            (SemaNumber::Rational(x), SemaNumber::Rational(y)) => Some(x.cmp(&y)),
452            _ => unreachable!("both exact after real_to_exact + promote"),
453        }
454    }
455}
456
457/// Format a real component the way Sema prints floats/ints (shared by the
458/// complex arm so `2.0+0.5i` matches standalone `2.0`/`0.5`).
459fn fmt_real(n: &SemaNumber, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
460    match n {
461        SemaNumber::Integer(v) => write!(f, "{v}"),
462        SemaNumber::Rational(r) => write!(f, "{}/{}", r.numer(), r.denom()),
463        SemaNumber::Real(v) => {
464            if v.fract() == 0.0 && v.is_finite() {
465                write!(f, "{v:.1}")
466            } else {
467                write!(f, "{v}")
468            }
469        }
470        SemaNumber::Complex(_) => unreachable!("complex component is never complex"),
471    }
472}
473
474impl std::fmt::Display for SemaNumber {
475    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476        use num_traits::Zero;
477        match self {
478            SemaNumber::Complex(c) => {
479                fmt_real(&c.re, f)?;
480                // Explicit sign then magnitude, so `0-1i` reads back correctly.
481                let (sign, mag) = match &c.im {
482                    SemaNumber::Integer(v) if *v < BigInt::zero() => {
483                        ('-', SemaNumber::Integer(-v.clone()))
484                    }
485                    SemaNumber::Rational(r) if *r < BigRational::from(BigInt::zero()) => {
486                        ('-', SemaNumber::Rational(-r.clone()))
487                    }
488                    SemaNumber::Real(v) if v.is_sign_negative() => ('-', SemaNumber::Real(-v)),
489                    other => ('+', other.clone()),
490                };
491                write!(f, "{sign}")?;
492                fmt_real(&mag, f)?;
493                write!(f, "i")
494            }
495            real => fmt_real(real, f),
496        }
497    }
498}
499
500impl SemaNumber {
501    pub fn from_i64(v: i64) -> SemaNumber {
502        SemaNumber::Integer(BigInt::from(v))
503    }
504    pub fn from_f64(v: f64) -> SemaNumber {
505        SemaNumber::Real(v)
506    }
507
508    pub fn to_inexact(self) -> SemaNumber {
509        match self {
510            SemaNumber::Complex(c) => SemaNumber::Complex(Box::new(Complex {
511                re: c.re.to_inexact(),
512                im: c.im.to_inexact(),
513            })),
514            other => SemaNumber::Real(other.to_f64()),
515        }
516    }
517
518    /// Convert inexact components to their exact rational value. Non-finite
519    /// reals have no exact value and are left as-is (callers that require
520    /// exactness should error; R7RS `inexact->exact` on ±inf/NaN is undefined).
521    pub fn to_exact(self) -> SemaNumber {
522        match self {
523            SemaNumber::Real(f) => SemaNumber::real_to_exact(f)
524                .map(|n| n.normalize())
525                .unwrap_or(SemaNumber::Real(f)),
526            SemaNumber::Complex(c) => SemaNumber::Complex(Box::new(Complex {
527                re: c.re.to_exact(),
528                im: c.im.to_exact(),
529            }))
530            .normalize(),
531            exact => exact,
532        }
533    }
534}
535
536/// The simplest rational number in the closed interval `[lo, hi]` (requires
537/// `lo <= hi`): the one with the smallest denominator, and among those the
538/// smallest numerator magnitude. Descends the Stern–Brocot tree via the
539/// continued-fraction expansion of the endpoints — this is the mathematical
540/// core of R7RS `rationalize`.
541fn simplest_rational_in(lo: BigRational, hi: BigRational) -> BigRational {
542    use num_traits::{Signed, Zero};
543    if lo.is_positive() {
544        simplest_positive(lo, hi)
545    } else if hi.is_negative() {
546        // Reflect the interval into the positive reals, solve, negate back.
547        -simplest_positive(-hi, -lo)
548    } else {
549        // 0 lies in `[lo, hi]` and is the simplest rational of all.
550        BigRational::zero()
551    }
552}
553
554/// Simplest rational in `[lo, hi]` assuming `0 < lo <= hi`.
555fn simplest_positive(lo: BigRational, hi: BigRational) -> BigRational {
556    use num_traits::One;
557    let fl = lo.floor(); // integer-valued BigRational
558    if fl == lo {
559        // `lo` is itself an integer — the simplest value in the interval.
560        fl
561    } else if fl < hi.floor() {
562        // An integer strictly above `lo` still lies at/below `hi`.
563        fl + BigRational::one()
564    } else {
565        // `lo` and `hi` share an integer part; recurse on the reciprocals of
566        // their fractional parts (reciprocation flips the ordering, so the new
567        // lower bound comes from `hi`).
568        let frac_lo = &lo - &fl;
569        let frac_hi = &hi - &fl;
570        fl + simplest_positive(frac_hi.recip(), frac_lo.recip()).recip()
571    }
572}
573
574impl SemaNumber {
575    /// Exact `BigRational` value of a *real* tower number, or `None` for a
576    /// non-finite `Real` (±inf / NaN) or a `Complex`.
577    fn to_exact_rational(&self) -> Option<BigRational> {
578        match self {
579            SemaNumber::Integer(i) => Some(BigRational::from(i.clone())),
580            SemaNumber::Rational(r) => Some(r.clone()),
581            SemaNumber::Real(f) => BigRational::from_float(*f),
582            SemaNumber::Complex(_) => None,
583        }
584    }
585
586    /// The simplest rational number within `tol` of `self` (R7RS
587    /// `rationalize`). Both operands must be real (the builtin guards complex).
588    /// Computes exactly over `BigRational` — the interval is
589    /// `[self - |tol|, self + |tol|]` — then applies inexactness contagion:
590    /// the result is inexact iff either operand is inexact.
591    pub fn rationalize(&self, tol: &SemaNumber) -> SemaNumber {
592        use num_traits::{Signed, Zero};
593        let inexact = !self.is_exact() || !tol.is_exact();
594        let wrap = |r: BigRational| {
595            let n = SemaNumber::Rational(r).normalize();
596            if inexact {
597                n.to_inexact()
598            } else {
599                n
600            }
601        };
602        // A non-finite value has no meaningful nearby rational — return it as-is.
603        let xr = match self.to_exact_rational() {
604            Some(x) => x,
605            None => return self.clone(),
606        };
607        let er = match tol.to_exact_rational() {
608            Some(e) => e.abs(),
609            // An infinite tolerance admits every rational ⇒ 0 is simplest; a
610            // NaN tolerance has no interval ⇒ leave the value unchanged.
611            None => {
612                return if matches!(tol, SemaNumber::Real(f) if f.is_infinite()) {
613                    wrap(BigRational::zero())
614                } else {
615                    self.clone()
616                };
617            }
618        };
619        let lo = &xr - &er;
620        let hi = &xr + &er;
621        wrap(simplest_rational_in(lo, hi))
622    }
623}
624
625impl PartialEq for SemaNumber {
626    fn eq(&self, other: &Self) -> bool {
627        use SemaNumber::*;
628        match (self, other) {
629            (Integer(a), Integer(b)) => a == b,
630            (Rational(a), Rational(b)) => a == b,
631            (Real(a), Real(b)) => a.to_bits() == b.to_bits(),
632            (Complex(a), Complex(b)) => a.re == b.re && a.im == b.im,
633            _ => false,
634        }
635    }
636}
637impl Eq for SemaNumber {}
638impl std::hash::Hash for SemaNumber {
639    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
640        use SemaNumber::*;
641        match self {
642            Integer(n) => {
643                0u8.hash(state);
644                n.hash(state);
645            }
646            Rational(r) => {
647                1u8.hash(state);
648                r.hash(state);
649            }
650            Real(f) => {
651                2u8.hash(state);
652                f.to_bits().hash(state);
653            }
654            Complex(c) => {
655                3u8.hash(state);
656                c.re.hash(state);
657                c.im.hash(state);
658            }
659        }
660    }
661}
662impl Ord for SemaNumber {
663    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
664        use SemaNumber::*;
665        self.level()
666            .cmp(&other.level())
667            .then_with(|| match (self, other) {
668                (Integer(a), Integer(b)) => a.cmp(b),
669                (Rational(a), Rational(b)) => a.cmp(b),
670                (Real(a), Real(b)) => a.total_cmp(b),
671                (Complex(a), Complex(b)) => a.re.cmp(&b.re).then_with(|| a.im.cmp(&b.im)),
672                _ => std::cmp::Ordering::Equal, // different levels already decided by level().cmp
673            })
674    }
675}
676impl PartialOrd for SemaNumber {
677    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
678        Some(self.cmp(other))
679    }
680}
681
682impl SemaNumber {
683    /// Parse an integer of arbitrary size in the given radix (2..=36). Accepts
684    /// an optional leading `+`/`-`. Returns `None` on any invalid digit.
685    pub fn parse_int_radix(digits: &str, radix: u32) -> Option<SemaNumber> {
686        let (sign, body) = match digits.strip_prefix('-') {
687            Some(rest) => (num_bigint::Sign::Minus, rest),
688            None => (
689                num_bigint::Sign::Plus,
690                digits.strip_prefix('+').unwrap_or(digits),
691            ),
692        };
693        if body.is_empty() {
694            return None;
695        }
696        let bytes = body.as_bytes();
697        let magnitude = num_bigint::BigUint::parse_bytes(bytes, radix)?;
698        Some(SemaNumber::Integer(BigInt::from_biguint(sign, magnitude)).normalize())
699    }
700
701    /// Parse `numer/denom` (decimal, sign on the numerator). `None` on a zero
702    /// denominator or invalid digits.
703    pub fn parse_rational(s: &str) -> Option<SemaNumber> {
704        use num_traits::Zero;
705        use std::str::FromStr;
706        let (n, d) = s.split_once('/')?;
707        let numer = BigInt::from_str(n).ok()?;
708        let denom = BigInt::from_str(d).ok()?;
709        if denom.is_zero() {
710            return None;
711        }
712        Some(SemaNumber::Rational(BigRational::new(numer, denom)).normalize())
713    }
714}
715
716#[cfg(test)]
717mod tests {
718    use super::*;
719    use num_traits::One;
720
721    #[test]
722    fn classification() {
723        assert!(SemaNumber::Integer(BigInt::from(5)).is_exact());
724        assert!(SemaNumber::Integer(BigInt::from(5)).is_integer());
725        assert!(!SemaNumber::Real(2.5).is_exact());
726        assert!(SemaNumber::Real(2.0).is_integer());
727        assert!(!SemaNumber::Real(2.5).is_integer());
728        let half = SemaNumber::Rational(BigRational::new(BigInt::one(), BigInt::from(2)));
729        assert!(half.is_exact());
730        assert!(!half.is_integer());
731        assert!(half.is_real());
732    }
733
734    #[test]
735    fn normalize_collapses() {
736        use num_traits::Zero;
737        // 4/2 → Integer(2)
738        let r = SemaNumber::Rational(BigRational::new(BigInt::from(4), BigInt::from(2)));
739        assert!(matches!(r.normalize(), SemaNumber::Integer(n) if n == BigInt::from(2)));
740        // 3 + 0i → Integer(3)
741        let c = SemaNumber::Complex(Box::new(Complex {
742            re: SemaNumber::Integer(BigInt::from(3)),
743            im: SemaNumber::Integer(BigInt::zero()),
744        }));
745        assert!(matches!(c.normalize(), SemaNumber::Integer(n) if n == BigInt::from(3)));
746        // 3 + 0.0i stays complex (0.0 is an INEXACT zero, not exact zero)
747        let c2 = SemaNumber::Complex(Box::new(Complex {
748            re: SemaNumber::Integer(BigInt::from(3)),
749            im: SemaNumber::Real(0.0),
750        }));
751        assert!(matches!(c2.normalize(), SemaNumber::Complex(_)));
752    }
753
754    #[test]
755    fn to_f64_projection() {
756        assert_eq!(SemaNumber::Integer(BigInt::from(7)).to_f64(), 7.0);
757        assert_eq!(
758            SemaNumber::Rational(BigRational::new(BigInt::one(), BigInt::from(4))).to_f64(),
759            0.25
760        );
761        assert_eq!(SemaNumber::Real(1.5).to_f64(), 1.5);
762    }
763
764    #[test]
765    fn promote_to_common_level() {
766        // Integer + Rational → both Rational
767        let (a, b) = SemaNumber::promote(
768            SemaNumber::Integer(BigInt::from(2)),
769            SemaNumber::Rational(BigRational::new(BigInt::one(), BigInt::from(2))),
770        );
771        assert!(matches!(a, SemaNumber::Rational(_)));
772        assert!(matches!(b, SemaNumber::Rational(_)));
773        // Integer + Real → both Real
774        let (a, b) =
775            SemaNumber::promote(SemaNumber::Integer(BigInt::from(2)), SemaNumber::Real(0.5));
776        assert!(matches!(a, SemaNumber::Real(_)));
777        assert!(matches!(b, SemaNumber::Real(_)));
778    }
779
780    #[test]
781    fn add_sub_mul_neg() {
782        use num_traits::Zero;
783        let two = || SemaNumber::Integer(BigInt::from(2));
784        let half = || SemaNumber::Rational(BigRational::new(BigInt::one(), BigInt::from(2)));
785        // 2 + 1/2 = 5/2
786        assert_eq!(two().add(half()).to_f64(), 2.5);
787        // exact: result is Rational, not Real
788        assert!(matches!(two().add(half()), SemaNumber::Rational(_)));
789        // 1/2 + 1/2 = 1 (normalizes to Integer)
790        assert!(matches!(half().add(half()), SemaNumber::Integer(n) if n == BigInt::one()));
791        // 2 - 2 = 0
792        assert!(matches!(two().sub(two()), SemaNumber::Integer(n) if n == BigInt::zero()));
793        // 2 * 1/2 = 1
794        assert!(matches!(two().mul(half()), SemaNumber::Integer(n) if n == BigInt::one()));
795        // -(1/2) = -1/2
796        assert_eq!(half().neg().to_f64(), -0.5);
797        // contagion: 2 + 0.5 = 2.5 as Real
798        assert!(matches!(
799            two().add(SemaNumber::Real(0.5)),
800            SemaNumber::Real(_)
801        ));
802    }
803
804    #[test]
805    fn abs_preserves_exactness_over_reals_inexact_over_complex() {
806        let n = |v: i64| SemaNumber::Integer(BigInt::from(v));
807        // exact integer stays exact
808        assert!(matches!(n(-5).abs(), SemaNumber::Integer(v) if v == BigInt::from(5)));
809        assert!(matches!(n(5).abs(), SemaNumber::Integer(v) if v == BigInt::from(5)));
810        // exact rational stays exact
811        let neg_half = SemaNumber::Rational(BigRational::new(BigInt::from(-1), BigInt::from(2)));
812        assert!(matches!(neg_half.abs(),
813            SemaNumber::Rational(r) if r == BigRational::new(BigInt::one(), BigInt::from(2))));
814        // inexact real stays inexact
815        assert!(matches!(SemaNumber::Real(-2.5).abs(), SemaNumber::Real(f) if f == 2.5));
816        // complex magnitude is the (inexact) hypot of its components
817        let c = SemaNumber::Complex(Box::new(Complex { re: n(3), im: n(4) }));
818        assert!(matches!(c.abs(), SemaNumber::Real(f) if f == 5.0));
819    }
820
821    #[test]
822    fn division_is_exact_when_possible() {
823        let n = |v: i64| SemaNumber::Integer(BigInt::from(v));
824        // 1 / 3 = 1/3 exact (NOT 0.333…)
825        let third = n(1).div(n(3)).unwrap();
826        assert!(matches!(&third, SemaNumber::Rational(r)
827            if *r == BigRational::new(BigInt::one(), BigInt::from(3))));
828        // 6 / 3 = 2 (normalizes to Integer)
829        assert!(matches!(n(6).div(n(3)).unwrap(), SemaNumber::Integer(k) if k == BigInt::from(2)));
830        // 1 / 2.0 = 0.5 (inexact contagion)
831        assert!(matches!(
832            n(1).div(SemaNumber::Real(2.0)).unwrap(),
833            SemaNumber::Real(_)
834        ));
835        // divide by exact zero → error
836        assert!(n(1).div(n(0)).is_err());
837        // divide by inexact zero → real infinity (IEEE), NOT an error
838        assert!(
839            matches!(n(1).div(SemaNumber::Real(0.0)).unwrap(), SemaNumber::Real(f) if f.is_infinite())
840        );
841    }
842
843    #[test]
844    fn compare_and_equal() {
845        use std::cmp::Ordering;
846        let n = |v: i64| SemaNumber::Integer(BigInt::from(v));
847        let half = SemaNumber::Rational(BigRational::new(BigInt::one(), BigInt::from(2)));
848        // 1/2 = 0.5 across exact/inexact
849        assert!(half.num_eq(&SemaNumber::Real(0.5)));
850        // 2 = 2.0
851        assert!(n(2).num_eq(&SemaNumber::Real(2.0)));
852        // ordering
853        assert_eq!(half.cmp_real(&n(1)), Some(Ordering::Less));
854        assert_eq!(n(3).cmp_real(&n(2)), Some(Ordering::Greater));
855        // exact bignum vs float above 2^53 stays exact (no lossy cast)
856        let big = SemaNumber::Integer(BigInt::from(9_007_199_254_740_993_i64));
857        assert_eq!(
858            big.cmp_real(&SemaNumber::Real(9_007_199_254_740_992.0)),
859            Some(Ordering::Greater)
860        );
861        // complex is unordered
862        let i = SemaNumber::Complex(Box::new(Complex { re: n(0), im: n(1) }));
863        assert_eq!(i.cmp_real(&n(0)), None);
864        assert!(!i.num_eq(&n(0)));
865    }
866
867    #[test]
868    fn display_round_trippable() {
869        let n = |v: i64| SemaNumber::Integer(BigInt::from(v));
870        assert_eq!(n(42).to_string(), "42");
871        assert_eq!(
872            SemaNumber::Rational(BigRational::new(BigInt::one(), BigInt::from(3))).to_string(),
873            "1/3"
874        );
875        assert_eq!(SemaNumber::Real(2.0).to_string(), "2.0");
876        assert_eq!(SemaNumber::Real(2.5).to_string(), "2.5");
877        let c = SemaNumber::Complex(Box::new(Complex { re: n(3), im: n(4) }));
878        assert_eq!(c.to_string(), "3+4i");
879        let c2 = SemaNumber::Complex(Box::new(Complex {
880            re: n(0),
881            im: n(-1),
882        }));
883        assert_eq!(c2.to_string(), "0-1i");
884    }
885
886    #[test]
887    fn exactness_conversions() {
888        let n = |v: i64| SemaNumber::Integer(BigInt::from(v));
889        // exact → inexact
890        assert!(matches!(n(3).to_inexact(), SemaNumber::Real(f) if f == 3.0));
891        // inexact 0.5 → exact 1/2
892        assert!(matches!(SemaNumber::Real(0.5).to_exact(),
893            SemaNumber::Rational(r) if r == BigRational::new(BigInt::one(), BigInt::from(2))));
894        // inexact 2.0 → exact 2 (normalizes to Integer)
895        assert!(
896            matches!(SemaNumber::Real(2.0).to_exact(), SemaNumber::Integer(k) if k == BigInt::from(2))
897        );
898        // bridges
899        assert!(matches!(SemaNumber::from_i64(5), SemaNumber::Integer(k) if k == BigInt::from(5)));
900        assert!(matches!(SemaNumber::from_f64(1.5), SemaNumber::Real(f) if f == 1.5));
901    }
902
903    #[test]
904    fn parse_literals() {
905        // arbitrary-precision decimal beyond i64
906        let big =
907            SemaNumber::parse_int_radix("170141183460469231731687303715884105728", 10).unwrap();
908        assert!(matches!(big, SemaNumber::Integer(_)));
909        // hex / binary
910        assert!(matches!(SemaNumber::parse_int_radix("ff", 16).unwrap(),
911            SemaNumber::Integer(n) if n == BigInt::from(255)));
912        assert!(matches!(SemaNumber::parse_int_radix("-101", 2).unwrap(),
913            SemaNumber::Integer(n) if n == BigInt::from(-5)));
914        // rational
915        assert!(matches!(SemaNumber::parse_rational("22/7").unwrap(),
916            SemaNumber::Rational(r) if r == BigRational::new(BigInt::from(22), BigInt::from(7))));
917        // 6/3 → normalizes to Integer 2
918        assert!(
919            matches!(SemaNumber::parse_rational("6/3").unwrap(), SemaNumber::Integer(n) if n == BigInt::from(2))
920        );
921        // rejects garbage
922        assert!(SemaNumber::parse_rational("1/0").is_none()); // zero denominator
923        assert!(SemaNumber::parse_int_radix("xyz", 16).is_none());
924    }
925
926    #[test]
927    fn rounding_preserves_exactness() {
928        let r = |n: i64, d: i64| {
929            SemaNumber::Rational(BigRational::new(BigInt::from(n), BigInt::from(d)))
930        };
931        // 7/2 = 3.5
932        assert!(matches!(r(7, 2).floor(), SemaNumber::Integer(n) if n == BigInt::from(3)));
933        assert!(matches!(r(7, 2).ceil(), SemaNumber::Integer(n) if n == BigInt::from(4)));
934        // banker's rounding: ties round to the nearest EVEN integer.
935        assert!(matches!(r(7, 2).round(), SemaNumber::Integer(n) if n == BigInt::from(4))); // 3.5 -> 4
936        assert!(matches!(r(5, 2).round(), SemaNumber::Integer(n) if n == BigInt::from(2))); // 2.5 -> 2
937        assert!(matches!(r(-5, 2).round(), SemaNumber::Integer(n) if n == BigInt::from(-2))); // -2.5 -> -2
938        assert!(matches!(r(-7, 2).truncate(), SemaNumber::Integer(n) if n == BigInt::from(-3)));
939        // integers pass through unchanged.
940        assert!(
941            matches!(SemaNumber::Integer(BigInt::from(5)).floor(), SemaNumber::Integer(n) if n == BigInt::from(5))
942        );
943        // reals stay inexact.
944        assert!(matches!(SemaNumber::Real(2.5).floor(), SemaNumber::Real(f) if f == 2.0));
945        assert!(matches!(SemaNumber::Real(2.5).round(), SemaNumber::Real(f) if f == 2.0));
946    }
947
948    #[test]
949    fn structural_traits() {
950        use std::collections::HashSet;
951        let a = SemaNumber::Integer(BigInt::from(3));
952        let b = SemaNumber::Integer(BigInt::from(3));
953        assert_eq!(a, b);
954        let mut set = HashSet::new();
955        set.insert(SemaNumber::Real(1.5));
956        assert!(set.contains(&SemaNumber::Real(1.5)));
957        // Ordering by level then value (used only for deterministic map keys).
958        assert!(SemaNumber::Integer(BigInt::from(1)) < SemaNumber::Real(0.0)); // level 0 < 2
959    }
960
961    #[test]
962    fn rationalize_simplest_in_interval() {
963        let n = |v: i64| SemaNumber::Integer(BigInt::from(v));
964        let r = |a: i64, b: i64| {
965            SemaNumber::Rational(BigRational::new(BigInt::from(a), BigInt::from(b)))
966        };
967        // 1/3 within 1/100 is already the simplest rational at that scale.
968        assert_eq!(r(1, 3).rationalize(&r(1, 100)), r(1, 3));
969        // Classic Scheme example: rationalize(3/10, 1/10) => 1/3.
970        assert_eq!(r(3, 10).rationalize(&r(1, 10)), r(1, 3));
971        // Exact inputs stay exact.
972        assert!(r(1, 3).rationalize(&r(1, 100)).is_exact());
973        // rationalize(5, 4): interval [1, 9] — simplest is the integer 1.
974        assert_eq!(n(5).rationalize(&n(4)), n(1));
975        // A tolerance spanning zero admits 0, the simplest rational of all.
976        assert_eq!(n(3).rationalize(&n(5)), n(0));
977        // Inexactness contagion: an inexact operand yields an inexact result.
978        assert!(!SemaNumber::Real(0.3).rationalize(&r(1, 10)).is_exact());
979        // A negative value: rationalize(-3/10, 1/10) => -1/3 (mirror image).
980        assert_eq!(r(-3, 10).rationalize(&r(1, 10)), r(-1, 3));
981    }
982
983    #[test]
984    fn powi_repeated_squaring() {
985        let n = |v: i64| SemaNumber::Integer(BigInt::from(v));
986        // 2^100 is exact and beyond i64 range.
987        let expected: BigInt = "1267650600228229401496703205376".parse().unwrap();
988        assert!(
989            matches!(n(2).powi(&BigInt::from(100)).unwrap(), SemaNumber::Integer(v) if v == expected)
990        );
991        // negative exponent -> exact reciprocal rational
992        assert!(matches!(n(2).powi(&BigInt::from(-3)).unwrap(),
993            SemaNumber::Rational(r) if r == BigRational::new(BigInt::one(), BigInt::from(8))));
994        // rational base
995        let half = SemaNumber::Rational(BigRational::new(BigInt::one(), BigInt::from(2)));
996        assert!(matches!(half.powi(&BigInt::from(3)).unwrap(),
997            SemaNumber::Rational(r) if r == BigRational::new(BigInt::one(), BigInt::from(8))));
998        // exponent 0 -> exact 1, even for base 0
999        assert!(
1000            matches!(n(0).powi(&BigInt::from(0)).unwrap(), SemaNumber::Integer(v) if v == BigInt::one())
1001        );
1002        // 0 base with negative exponent divides by zero
1003        assert!(n(0).powi(&BigInt::from(-1)).is_err());
1004    }
1005}