Skip to main content

oxinum_float/native/
nonfinite.rs

1//! IEEE-754 non-finite propagation helpers for native [`BigFloat`] arithmetic.
2//!
3//! Two entry points:
4//! - [`nonfinite_binop`] — used by the arithmetic **operators** (`+`, `-`, `*`, `/`, `%`).
5//!   Handles both NaN/Inf *propagation* and non-finite *generation* (e.g. `finite / 0`
6//!   produces `±Inf`; `0 / 0` or `Inf - Inf` produces `NaN`).
7//! - [`nonfinite_propagate`] — input-propagation subset only (no generation from finite
8//!   inputs). Used by the **checked methods** (`div_ref`, `sqrt_ref`, …) so they keep
9//!   their `Err(DivByZero)` / `Err(Domain)` contract on finite-domain errors while still
10//!   propagating non-finite inputs.
11//!
12//! # Rem special case
13//!
14//! For `Rem`, when `rhs` is `Infinite` and `lhs` is finite, `nonfinite_binop` returns
15//! `None` — the caller should return `lhs.clone()` (IEEE rule: `finite % Inf = finite`).
16
17use oxinum_core::Sign;
18
19use super::float::{BigFloat, FloatClass};
20
21/// Describes which binary operation is being performed.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum BinOp {
24    Add,
25    Sub,
26    Mul,
27    Div,
28    Rem,
29}
30
31/// IEEE-754 binary operation table for non-finite inputs **or** non-finite results
32/// generated from finite inputs (zero divisors).
33///
34/// Returns `Some(result)` when the result is determined by IEEE rules (either operand
35/// is non-finite, or `op` generates non-finite from finite inputs such as `finite / 0`).
36/// Returns `None` when both operands are finite and `op` on finite inputs is well-defined
37/// (the caller should proceed with normal computation).
38///
39/// # Rem special case
40///
41/// For `Rem`, when `rhs` is `Infinite` and `lhs` is finite, returns `None` — the caller
42/// should return `lhs.clone()` (IEEE rule: `finite % Inf = finite`).
43pub fn nonfinite_binop(lhs: &BigFloat, rhs: &BigFloat, op: BinOp) -> Option<BigFloat> {
44    let prec = lhs.precision.max(rhs.precision);
45
46    // Universal NaN propagation: any NaN operand → NaN.
47    if lhs.is_nan() || rhs.is_nan() {
48        return Some(BigFloat::nan(prec));
49    }
50
51    match op {
52        BinOp::Add => nonfinite_add(lhs, rhs, prec),
53        BinOp::Sub => {
54            // a - b == a + (-b): negate rhs and route through add.
55            let neg_rhs = rhs.neg();
56            nonfinite_add(lhs, &neg_rhs, prec)
57        }
58        BinOp::Mul => nonfinite_mul(lhs, rhs, prec),
59        BinOp::Div => nonfinite_div(lhs, rhs, prec, true),
60        BinOp::Rem => nonfinite_rem(lhs, rhs, prec),
61    }
62}
63
64/// Input-propagation subset only: propagates NaN/Inf inputs; does **not** generate
65/// Inf from finite-zero divisors (the caller keeps `Err(DivByZero)` for that case).
66///
67/// Returns `Some(result)` when an input is non-finite; `None` when both are finite.
68pub fn nonfinite_propagate(lhs: &BigFloat, rhs: &BigFloat, op: BinOp) -> Option<BigFloat> {
69    let prec = lhs.precision.max(rhs.precision);
70
71    if lhs.is_nan() || rhs.is_nan() {
72        return Some(BigFloat::nan(prec));
73    }
74
75    if lhs.is_finite() && rhs.is_finite() {
76        return None; // Both finite — checked method handles domain errors itself.
77    }
78
79    // At least one operand is Infinite; route to the full table but skip
80    // the finite-zero-divisor generation in Div (pass `generate_from_finite = false`).
81    match op {
82        BinOp::Add => nonfinite_add(lhs, rhs, prec),
83        BinOp::Sub => {
84            let neg_rhs = rhs.neg();
85            nonfinite_add(lhs, &neg_rhs, prec)
86        }
87        BinOp::Mul => nonfinite_mul(lhs, rhs, prec),
88        BinOp::Div => nonfinite_div(lhs, rhs, prec, false),
89        BinOp::Rem => nonfinite_rem(lhs, rhs, prec),
90    }
91}
92
93// ---------------------------------------------------------------------------
94// Per-operation tables
95// ---------------------------------------------------------------------------
96
97fn nonfinite_add(lhs: &BigFloat, rhs: &BigFloat, prec: u32) -> Option<BigFloat> {
98    match (lhs.class, rhs.class) {
99        // Both Inf: same sign → Inf; opposite signs → NaN.
100        (FloatClass::Infinite, FloatClass::Infinite) => {
101            if lhs.sign == rhs.sign {
102                Some(signed_inf(lhs.sign, prec))
103            } else {
104                Some(BigFloat::nan(prec))
105            }
106        }
107        // One Inf, one finite: result is the Inf.
108        (FloatClass::Infinite, FloatClass::Finite) => Some(signed_inf(lhs.sign, prec)),
109        (FloatClass::Finite, FloatClass::Infinite) => Some(signed_inf(rhs.sign, prec)),
110        // Both finite: caller handles.
111        (FloatClass::Finite, FloatClass::Finite) => None,
112        // NaN already handled in the caller.
113        _ => unreachable!("NaN should have been handled before reaching nonfinite_add"),
114    }
115}
116
117fn nonfinite_mul(lhs: &BigFloat, rhs: &BigFloat, prec: u32) -> Option<BigFloat> {
118    match (lhs.class, rhs.class) {
119        // Inf * 0 or 0 * Inf → NaN.
120        (FloatClass::Infinite, FloatClass::Finite) if rhs.is_zero() => Some(BigFloat::nan(prec)),
121        (FloatClass::Finite, FloatClass::Infinite) if lhs.is_zero() => Some(BigFloat::nan(prec)),
122        // Inf * Inf or Inf * nonzero-finite → ±Inf (sign = XOR of signs).
123        (FloatClass::Infinite, FloatClass::Infinite)
124        | (FloatClass::Infinite, FloatClass::Finite)
125        | (FloatClass::Finite, FloatClass::Infinite) => {
126            let sign = xor_sign(lhs.sign, rhs.sign);
127            Some(signed_inf(sign, prec))
128        }
129        // Both finite: caller handles.
130        (FloatClass::Finite, FloatClass::Finite) => None,
131        _ => unreachable!("NaN should have been handled before reaching nonfinite_mul"),
132    }
133}
134
135fn nonfinite_div(
136    lhs: &BigFloat,
137    rhs: &BigFloat,
138    prec: u32,
139    generate_from_finite: bool,
140) -> Option<BigFloat> {
141    match (lhs.class, rhs.class) {
142        // Inf / Inf = NaN.
143        (FloatClass::Infinite, FloatClass::Infinite) => Some(BigFloat::nan(prec)),
144        // Inf / finite.
145        (FloatClass::Infinite, FloatClass::Finite) => {
146            // Inf / 0 = NaN (indeterminate).
147            if rhs.is_zero() {
148                Some(BigFloat::nan(prec))
149            } else {
150                Some(signed_inf(xor_sign(lhs.sign, rhs.sign), prec))
151            }
152        }
153        // finite / Inf = ±0 (sign from XOR, but we use canonical +0).
154        (FloatClass::Finite, FloatClass::Infinite) => Some(BigFloat::zero(prec)),
155        // finite / finite.
156        (FloatClass::Finite, FloatClass::Finite) => {
157            if generate_from_finite && rhs.is_zero() {
158                // finite / 0 → ±Inf (sign from numerator; canonical zero has no sign).
159                if lhs.is_zero() {
160                    // 0 / 0 = NaN.
161                    Some(BigFloat::nan(prec))
162                } else {
163                    Some(signed_inf(lhs.sign, prec))
164                }
165            } else {
166                None // Caller handles (either Err(DivByZero) or normal division).
167            }
168        }
169        _ => unreachable!("NaN should have been handled before reaching nonfinite_div"),
170    }
171}
172
173fn nonfinite_rem(lhs: &BigFloat, rhs: &BigFloat, prec: u32) -> Option<BigFloat> {
174    match (lhs.class, rhs.class) {
175        // Inf % anything = NaN.
176        (FloatClass::Infinite, _) => Some(BigFloat::nan(prec)),
177        // anything % 0 = NaN.
178        (FloatClass::Finite, FloatClass::Finite) if rhs.is_zero() => Some(BigFloat::nan(prec)),
179        // finite % Inf: IEEE says the result is the finite value (lhs unchanged).
180        // Return None to signal "caller should return lhs.clone()".
181        // See module-level docs for Wave 2 contract.
182        (FloatClass::Finite, FloatClass::Infinite) => None,
183        // Both finite, rhs nonzero: caller handles.
184        (FloatClass::Finite, FloatClass::Finite) => None,
185        _ => unreachable!("NaN should have been handled before reaching nonfinite_rem"),
186    }
187}
188
189// ---------------------------------------------------------------------------
190// Sign helpers
191// ---------------------------------------------------------------------------
192
193fn xor_sign(a: Sign, b: Sign) -> Sign {
194    if a == b {
195        Sign::Positive
196    } else {
197        Sign::Negative
198    }
199}
200
201fn signed_inf(sign: Sign, prec: u32) -> BigFloat {
202    if sign == Sign::Negative {
203        BigFloat::neg_infinity(prec)
204    } else {
205        BigFloat::infinity(prec)
206    }
207}
208
209// ---------------------------------------------------------------------------
210// Tests — also serve to suppress dead_code warnings on Wave-1 items that
211// Wave-2 operator files will consume once they are written.
212// ---------------------------------------------------------------------------
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use crate::native::{BigFloat, RoundingMode};
218
219    const P: u32 = 53;
220
221    fn finite(v: i64) -> BigFloat {
222        BigFloat::from_i64(v, P, RoundingMode::HalfEven)
223    }
224
225    // -----------------------------------------------------------------------
226    // nonfinite_binop: Add
227    // -----------------------------------------------------------------------
228
229    #[test]
230    fn add_nan_propagates() {
231        let nan = BigFloat::nan(P);
232        let fin = finite(1);
233        let r = nonfinite_binop(&nan, &fin, BinOp::Add);
234        assert!(r.expect("should produce NaN").is_nan());
235        let r2 = nonfinite_binop(&fin, &nan, BinOp::Add);
236        assert!(r2.expect("should produce NaN").is_nan());
237    }
238
239    #[test]
240    fn add_inf_plus_inf_same_sign() {
241        let pos_inf = BigFloat::infinity(P);
242        let r =
243            nonfinite_binop(&pos_inf, &pos_inf, BinOp::Add).expect("+inf + +inf should be +inf");
244        assert!(r.is_infinite());
245        assert!(r.is_sign_positive());
246    }
247
248    #[test]
249    fn add_inf_plus_neg_inf_is_nan() {
250        let pos_inf = BigFloat::infinity(P);
251        let neg_inf = BigFloat::neg_infinity(P);
252        let r = nonfinite_binop(&pos_inf, &neg_inf, BinOp::Add).expect("+inf + -inf should be NaN");
253        assert!(r.is_nan());
254    }
255
256    #[test]
257    fn add_inf_plus_finite() {
258        let pos_inf = BigFloat::infinity(P);
259        let fin = finite(42);
260        let r = nonfinite_binop(&pos_inf, &fin, BinOp::Add).expect("+inf + finite should be +inf");
261        assert!(r.is_infinite() && r.is_sign_positive());
262    }
263
264    #[test]
265    fn add_both_finite_returns_none() {
266        let a = finite(1);
267        let b = finite(2);
268        assert!(nonfinite_binop(&a, &b, BinOp::Add).is_none());
269    }
270
271    // -----------------------------------------------------------------------
272    // nonfinite_binop: Sub (routes through neg + add)
273    // -----------------------------------------------------------------------
274
275    #[test]
276    fn sub_inf_minus_inf_same_sign_is_nan() {
277        let pos_inf = BigFloat::infinity(P);
278        let r = nonfinite_binop(&pos_inf, &pos_inf, BinOp::Sub).expect("+inf - +inf should be NaN");
279        assert!(r.is_nan());
280    }
281
282    // -----------------------------------------------------------------------
283    // nonfinite_binop: Mul
284    // -----------------------------------------------------------------------
285
286    #[test]
287    fn mul_inf_times_zero_is_nan() {
288        let pos_inf = BigFloat::infinity(P);
289        let zero = BigFloat::zero(P);
290        let r = nonfinite_binop(&pos_inf, &zero, BinOp::Mul).expect("inf * 0 should be NaN");
291        assert!(r.is_nan());
292    }
293
294    #[test]
295    fn mul_inf_times_nonzero_finite() {
296        let pos_inf = BigFloat::infinity(P);
297        let fin = finite(3);
298        let r = nonfinite_binop(&pos_inf, &fin, BinOp::Mul).expect("+inf * 3 should be +inf");
299        assert!(r.is_infinite() && r.is_sign_positive());
300    }
301
302    #[test]
303    fn mul_pos_inf_times_neg_finite() {
304        let pos_inf = BigFloat::infinity(P);
305        let neg = finite(-3);
306        let r = nonfinite_binop(&pos_inf, &neg, BinOp::Mul).expect("+inf * -3 should be -inf");
307        assert!(r.is_infinite() && r.is_sign_negative());
308    }
309
310    #[test]
311    fn mul_inf_times_inf() {
312        let pos_inf = BigFloat::infinity(P);
313        let neg_inf = BigFloat::neg_infinity(P);
314        let r =
315            nonfinite_binop(&pos_inf, &neg_inf, BinOp::Mul).expect("+inf * -inf should be -inf");
316        assert!(r.is_infinite() && r.is_sign_negative());
317    }
318
319    // -----------------------------------------------------------------------
320    // nonfinite_binop: Div
321    // -----------------------------------------------------------------------
322
323    #[test]
324    fn div_inf_over_inf_is_nan() {
325        let pos_inf = BigFloat::infinity(P);
326        let r = nonfinite_binop(&pos_inf, &pos_inf, BinOp::Div).expect("inf/inf should be NaN");
327        assert!(r.is_nan());
328    }
329
330    #[test]
331    fn div_finite_over_zero() {
332        let fin = finite(5);
333        let zero = BigFloat::zero(P);
334        let r = nonfinite_binop(&fin, &zero, BinOp::Div).expect("5/0 should be +inf");
335        assert!(r.is_infinite() && r.is_sign_positive());
336    }
337
338    #[test]
339    fn div_zero_over_zero_is_nan() {
340        let zero = BigFloat::zero(P);
341        let r = nonfinite_binop(&zero, &zero, BinOp::Div).expect("0/0 should be NaN");
342        assert!(r.is_nan());
343    }
344
345    #[test]
346    fn div_finite_over_inf_is_zero() {
347        let fin = finite(7);
348        let pos_inf = BigFloat::infinity(P);
349        let r = nonfinite_binop(&fin, &pos_inf, BinOp::Div).expect("7/inf should be zero");
350        assert!(r.is_zero());
351    }
352
353    // -----------------------------------------------------------------------
354    // nonfinite_binop: Rem
355    // -----------------------------------------------------------------------
356
357    #[test]
358    fn rem_inf_mod_anything_is_nan() {
359        let pos_inf = BigFloat::infinity(P);
360        let fin = finite(3);
361        let r = nonfinite_binop(&pos_inf, &fin, BinOp::Rem).expect("inf % 3 should be NaN");
362        assert!(r.is_nan());
363    }
364
365    #[test]
366    fn rem_finite_mod_zero_is_nan() {
367        let fin = finite(5);
368        let zero = BigFloat::zero(P);
369        let r = nonfinite_binop(&fin, &zero, BinOp::Rem).expect("5 % 0 should be NaN");
370        assert!(r.is_nan());
371    }
372
373    #[test]
374    fn rem_finite_mod_inf_returns_none() {
375        // Caller should return lhs.clone() (IEEE: finite % Inf = finite).
376        let fin = finite(5);
377        let pos_inf = BigFloat::infinity(P);
378        let r = nonfinite_binop(&fin, &pos_inf, BinOp::Rem);
379        assert!(
380            r.is_none(),
381            "finite % inf should return None (caller returns lhs)"
382        );
383    }
384
385    // -----------------------------------------------------------------------
386    // nonfinite_propagate
387    // -----------------------------------------------------------------------
388
389    #[test]
390    fn propagate_both_finite_returns_none() {
391        let a = finite(1);
392        let b = finite(2);
393        assert!(nonfinite_propagate(&a, &b, BinOp::Add).is_none());
394    }
395
396    #[test]
397    fn propagate_nan_returns_nan() {
398        let nan = BigFloat::nan(P);
399        let fin = finite(1);
400        assert!(nonfinite_propagate(&nan, &fin, BinOp::Add)
401            .expect("should be NaN")
402            .is_nan());
403    }
404
405    #[test]
406    fn propagate_div_finite_over_zero_returns_none() {
407        // nonfinite_propagate should NOT generate Inf from finite/0 — that's
408        // the caller's job (Err(DivByZero)).
409        let fin = finite(5);
410        let zero = BigFloat::zero(P);
411        assert!(nonfinite_propagate(&fin, &zero, BinOp::Div).is_none());
412    }
413}