Skip to main content

dashu_float/
add.rs

1use crate::{
2    error::{assert_finite_operands, FpError, FpResult},
3    fbig::FBig,
4    helper_macros,
5    repr::{Context, Repr, Word},
6    round::{Round, Rounded},
7    utils::{digit_len, shl_digits, shl_digits_in_place, split_digits, split_digits_ref},
8};
9use core::{
10    cmp::Ordering,
11    ops::{Add, AddAssign, Sub, SubAssign},
12};
13
14use dashu_base::Sign::{self, *};
15use dashu_base::{Abs, EstimatedLog2};
16use dashu_int::{IBig, UBig};
17
18/// Build a `Repr` from a cancellation result, producing `-0` (instead of `+0`) when the
19/// significand is zero and the rounding mode is roundTowardNegative (IEEE 754 §6.3).
20pub(crate) fn cancel_zero<R: Round, const B: Word>(sig: IBig, exp: isize) -> Repr<B> {
21    if sig.is_zero() && R::IS_ROUND_TOWARD_NEGATIVE {
22        Repr::neg_zero()
23    } else {
24        Repr::new(sig, exp)
25    }
26}
27
28/// The signed zero resulting from a sum (or difference) of two *zero* operands, per
29/// IEEE 754 §6.3: `x + x = x − (−x)` retains `x`'s sign, so `(−0) + (−0)` is `−0`; a sum of
30/// zeros of *mixed* sign takes the sign of zero under roundTowardNegative and `+0` otherwise.
31///
32/// `lhs_neg` / `rhs_eff_neg` are the two operand signs, the second already carrying the
33/// operation's sign (`−(−0)` is `+0`). Cancellation of *nonzero* operands goes through
34/// [`cancel_zero`] instead, which needs neither sign — the operands being equal and opposite
35/// already decides it.
36fn zero_sum<R: Round, const B: Word>(lhs_neg: bool, rhs_eff_neg: bool) -> Repr<B> {
37    let neg = match (lhs_neg, rhs_eff_neg) {
38        (true, true) => true, // x + x retains x's sign
39        (false, false) => false,
40        _ => R::IS_ROUND_TOWARD_NEGATIVE, // mixed signs
41    };
42    if neg {
43        Repr::neg_zero()
44    } else {
45        Repr::zero()
46    }
47}
48
49impl<R: Round, const B: Word> Add for FBig<R, B> {
50    type Output = Self;
51
52    #[inline]
53    fn add(self, rhs: Self) -> Self::Output {
54        let context = Context::max(self.context, rhs.context);
55        let sum = context.addsub_vv(self.repr, rhs.repr, Positive);
56        FBig::new(sum.value(), context)
57    }
58}
59
60impl<R: Round, const B: Word> Add<&FBig<R, B>> for FBig<R, B> {
61    type Output = Self;
62
63    #[inline]
64    fn add(self, rhs: &FBig<R, B>) -> Self::Output {
65        let context = Context::max(self.context, rhs.context);
66        let sum = context.addsub_vr(self.repr, &rhs.repr, Positive);
67        FBig::new(sum.value(), context)
68    }
69}
70
71impl<R: Round, const B: Word> Add<FBig<R, B>> for &FBig<R, B> {
72    type Output = FBig<R, B>;
73
74    #[inline]
75    fn add(self, rhs: FBig<R, B>) -> Self::Output {
76        let context = Context::max(self.context, rhs.context);
77        let sum = context.addsub_rv(&self.repr, rhs.repr, Positive);
78        FBig::new(sum.value(), context)
79    }
80}
81
82impl<R: Round, const B: Word> Add<&FBig<R, B>> for &FBig<R, B> {
83    type Output = FBig<R, B>;
84
85    #[inline]
86    fn add(self, rhs: &FBig<R, B>) -> Self::Output {
87        let context = Context::max(self.context, rhs.context);
88        let sum = context.addsub_rr(&self.repr, &rhs.repr, Positive);
89        FBig::new(sum.value(), context)
90    }
91}
92
93impl<R: Round, const B: Word> Sub for FBig<R, B> {
94    type Output = Self;
95
96    #[inline]
97    fn sub(self, rhs: Self) -> Self::Output {
98        let context = Context::max(self.context, rhs.context);
99        let sum = context.addsub_vv(self.repr, rhs.repr, Negative);
100        FBig::new(sum.value(), context)
101    }
102}
103
104impl<R: Round, const B: Word> Sub<&FBig<R, B>> for FBig<R, B> {
105    type Output = Self;
106
107    #[inline]
108    fn sub(self, rhs: &FBig<R, B>) -> Self::Output {
109        let context = Context::max(self.context, rhs.context);
110        let sum = context.addsub_vr(self.repr, &rhs.repr, Negative);
111        FBig::new(sum.value(), context)
112    }
113}
114
115impl<R: Round, const B: Word> Sub<FBig<R, B>> for &FBig<R, B> {
116    type Output = FBig<R, B>;
117
118    #[inline]
119    fn sub(self, rhs: FBig<R, B>) -> Self::Output {
120        let context = Context::max(self.context, rhs.context);
121        let sum = context.addsub_rv(&self.repr, rhs.repr, Negative);
122        FBig::new(sum.value(), context)
123    }
124}
125
126impl<R: Round, const B: Word> Sub<&FBig<R, B>> for &FBig<R, B> {
127    type Output = FBig<R, B>;
128
129    #[inline]
130    fn sub(self, rhs: &FBig<R, B>) -> Self::Output {
131        let context = Context::max(self.context, rhs.context);
132        let sum = context.addsub_rr(&self.repr, &rhs.repr, Negative);
133        FBig::new(sum.value(), context)
134    }
135}
136
137helper_macros::impl_binop_assign_by_taking!(impl AddAssign<Self>, add_assign, add);
138helper_macros::impl_binop_assign_by_taking!(impl SubAssign<Self>, sub_assign, sub);
139
140macro_rules! impl_add_sub_primitive_with_fbig {
141    ($($t:ty)*) => {$(
142        helper_macros::impl_binop_with_primitive!(impl Add<$t>, add);
143        helper_macros::impl_binop_assign_with_primitive!(impl AddAssign<$t>, add_assign);
144        helper_macros::impl_binop_with_primitive!(impl Sub<$t>, sub);
145        helper_macros::impl_binop_assign_with_primitive!(impl SubAssign<$t>, sub_assign);
146    )*};
147}
148impl_add_sub_primitive_with_fbig!(u8 u16 u32 u64 u128 usize UBig i8 i16 i32 i64 i128 isize IBig);
149
150impl<R: Round> Context<R> {
151    /// Round sum = `significand * B ^ exponent` with the low part (value, precision).
152    /// If the sum is actually from a subtraction and the low part is not zero, `is_sub` should be true.
153    pub(crate) fn repr_round_sum<const B: Word>(
154        &self,
155        mut significand: IBig,
156        mut exponent: isize,
157        mut low: (IBig, usize),
158        is_sub: bool,
159    ) -> Rounded<Repr<B>> {
160        // A zero produced by exact cancellation is -0 only under roundTowardNegative (Down),
161        // +0 otherwise (IEEE 754 §6.3).
162        let neg_cancel = is_sub && R::IS_ROUND_TOWARD_NEGATIVE;
163        let make_repr = |sig: IBig, exp: isize| -> Repr<B> {
164            if sig.is_zero() && neg_cancel {
165                Repr::neg_zero()
166            } else {
167                Repr::new(sig, exp)
168            }
169        };
170
171        if !self.is_limited() {
172            // short cut for unlimited precision
173            return Rounded::Exact(make_repr(significand, exponent));
174        }
175
176        // use one extra digit to prevent cancellation in rounding
177        let rnd_precision = self.precision + is_sub as usize;
178
179        // An astronomically-low low part — one that sits entirely below the rounding window
180        // (its value under-estimates B^(low.1 − window), so no digit of it reaches the window,
181        // only its sticky sign matters) — is collapsed to a unit at a bounded position. The
182        // digit alignment below shifts by `low.1`, which for exponent gaps of ~10⁹+ would
183        // otherwise materialize B^(low.1) digits and die on an out-of-memory allocation
184        // (e.g. the two exponentials of `sinh(1e11)` sit ~5·10¹¹ digits apart). The collapse
185        // is sound: both the original fraction and the unit replacement lie strictly between
186        // 0 and ½ ulp (window ≥ rnd_precision + 2 ≥ 2), so every rounding mode decides
187        // identically on either.
188        let window = rnd_precision + 2;
189        if !low.0.is_zero() && low.1 > window {
190            // Sound side: proving `|low.0| < B^(low.1 − window)` from bounds needs the
191            // *lower* bound of log₂B on the right — an upper bound would let a low part that
192            // still reaches the window collapse. The margin absorbs the f32 slack of both
193            // bound products (relative 2⁻²³ of a ~`low.1·log₂B`-sized value, i.e. up to
194            // ~`low.1/2²¹` digit positions; the +64 floors it for small gaps).
195            let (b_lb, _) = B.log2_bounds();
196            let margin = (low.1 >> 21) + 64;
197            let gap = (low.1 - window).saturating_sub(margin);
198            if low.0.clone().abs().log2_bounds().1 < gap as f32 * b_lb {
199                low = (low.0.signum(), window);
200            }
201        }
202
203        // align to precision again
204        let digits = digit_len::<B>(&significand);
205        match digits.cmp(&rnd_precision) {
206            Ordering::Equal => {}
207            Ordering::Greater => {
208                // Shrink if the result has more digits than desired precision
209                /*
210                 * lhs:         |=========0000|
211                 * rhs:              |========|xxxxx|
212                 * sum:        |==============|xxxxx|
213                 * precision:  |<----->|
214                 * shrink:     |=======|xxxxxxxxxxxx|
215                 */
216                let shift = digits - rnd_precision;
217                let (signif_hi, mut signif_lo) = split_digits::<B>(significand, shift);
218                significand = signif_hi;
219                exponent += shift as isize;
220                shl_digits_in_place::<B>(&mut signif_lo, low.1);
221                low.0 += signif_lo;
222                low.1 += shift;
223            }
224            Ordering::Less => {
225                // Expand to low parts if the result has less digits than desired precision.
226                /*
227                 * A possible case when lhs and rhs have different sign:
228                 * lhs:  |=========0000|
229                 * rhs:  |=============|xxxxx|
230                 * sum:          |=====|xxxxx|
231                 * precision+1:  |<------>|
232                 * shift:              |<>|
233                 * expanded:     |========|xx|
234                 */
235                if !low.0.is_zero() {
236                    let (low_val, low_prec) = low;
237                    let shift = low_prec.min(rnd_precision - digits);
238                    let (pad, low_val) = split_digits::<B>(low_val, low_prec - shift);
239                    shl_digits_in_place::<B>(&mut significand, shift);
240                    exponent -= shift as isize;
241                    significand += pad;
242                    low = (low_val, low_prec - shift);
243                }
244            }
245        };
246
247        // perform rounding
248        if low.0.is_zero() {
249            Rounded::Exact(make_repr(significand, exponent))
250        } else {
251            // By now significand should have at least full precision. After adjustment, the digits length
252            // could be one more than the precision. We don't shrink the extra digit.
253            let adjust = R::round_fract::<B>(&significand, low.0, low.1);
254            Rounded::Inexact(make_repr(significand + adjust, exponent), adjust)
255        }
256    }
257
258    // lhs + rhs_sign * rhs, assuming lhs.exponent >= rhs.exponent
259    pub(crate) fn repr_add_large_small<const B: Word>(
260        &self,
261        mut lhs: Repr<B>,
262        rhs: &Repr<B>,
263        rhs_sign: Sign,
264    ) -> Rounded<Repr<B>> {
265        debug_assert!(lhs.exponent >= rhs.exponent);
266
267        // use one extra digit when subtracting to prevent cancellation in rounding
268        let is_sub = lhs.significand.sign() != rhs_sign * rhs.significand.sign();
269        let rnd_precision = self.precision + is_sub as usize;
270
271        let ediff = (lhs.exponent - rhs.exponent) as usize;
272        let ldigits = lhs.digits();
273        let rdigits_est = rhs.digits_ub(); // overestimate
274
275        // align the exponent
276        let low: (IBig, usize); // (value of low part, precision of the low part)
277        let (significand, exponent) =
278            if self.is_limited() && is_sub && rdigits_est + self.precision >= ldigits + ediff {
279                // The smaller operand (`rhs`, lower exponent) reaches the larger
280                // operand's `precision`-digit window — its top digit is at or above the
281                // window edge (`rdigits + precision >= ldigits + ediff`, i.e. `rhs`'s top
282                // position `rdigits - ediff` is `>= ldigits - precision`). An effective
283                // subtraction can then cancel and lose leading digits, which the trimmed
284                // path cannot recover (its single re-expand in `repr_round_sum` collapses
285                // a genuinely small difference to the wrong value — e.g. `1.00 -
286                // 0.99999999` at precision 3 to `0` instead of `1e-8`, or `0.5 - 0.4375`
287                // at precision 1 to `0` instead of `0.0625`). So form the exact difference
288                // at full operand width and let the shared `repr_round_sum` round it once
289                // (with the same guard digit as the trimmed path, so no low tail is
290                // needed). The complement (`<`) is the trimmed/negligible region where
291                // `rhs` stays strictly below the window and no cancellation is possible.
292                shl_digits_in_place::<B>(&mut lhs.significand, ediff);
293                low = (IBig::ZERO, 0);
294                match rhs_sign {
295                    Positive => (lhs.significand + &rhs.significand, rhs.exponent),
296                    Negative => (lhs.significand - &rhs.significand, rhs.exponent),
297                }
298            } else if self.is_limited()
299                && rdigits_est + 1 < ediff
300                && rdigits_est + 1 + rnd_precision < ldigits + ediff
301            {
302                // rhs is entirely below lhs's rounding window, so only its sign
303                // contributes to the rounding; replace it with a unit sticky tail
304                // (`|low| = 1`).
305                //
306                // The sticky must be positioned at rhs's *real* magnitude, i.e. `ediff`
307                // digits below lhs's exponent — NOT at `precision - ldigits`. Positioning
308                // by `ediff` keeps the sticky genuinely sub-ULP (|1| << B^ediff, and the
309                // branch guard guarantees ediff >= 3), so it can never land on a rounding
310                // tie. Positioning by `precision - ldigits` instead let the re-expand drag
311                // the sticky up to the LSB, where for base 2 + round-to-nearest it equals
312                // exactly half (1 == B^0 == ½·B^1) and injected a spurious ULP — e.g.
313                // `1 + 2^-100` at precision 10 returned `513·2^-9` instead of `1`.
314                low = (rhs_sign * rhs.significand.signum(), ediff);
315                (lhs.significand, lhs.exponent)
316            } else if self.is_limited() && ldigits >= self.precision {
317                // if the lhs already exceeds the desired precision, just align rhs
318                /* Before:
319                 * lhs: |==============|
320                 * rhs:      |==============|
321                 *              ediff  |<-->|
322                 *    precision  |<--->|
323                 *
324                 * After:
325                 * lhs: |==============|
326                 * rhs:      |=========|xxxx|
327                 */
328                let (rhs_signif, r) = split_digits_ref::<B>(&rhs.significand, ediff);
329                low = (rhs_sign * r, ediff);
330                (lhs.significand + rhs_sign * rhs_signif, lhs.exponent)
331            } else if self.is_limited() && ediff + ldigits > self.precision {
332                // if the shifted lhs exceeds the desired precision, align lhs and rhs to precision
333                /* Before:
334                 * lhs: |=========|
335                 * rhs:      |==============|
336                 *                |< ediff >|
337                 *      |< precision >|
338                 *
339                 * After:
340                 * lhs: |=========0000|
341                 * rhs:      |========|xxxxx|
342                 *        lshift  |<->|
343                 *            rshift  |<--->|
344                 */
345                let lshift = self.precision - ldigits;
346                let rshift = ediff - lshift;
347                let (rhs_signif, r) = split_digits_ref::<B>(&rhs.significand, rshift);
348                shl_digits_in_place::<B>(&mut lhs.significand, lshift);
349
350                low = (rhs_sign * r, rshift);
351                (lhs.significand + rhs_sign * rhs_signif, lhs.exponent - lshift as isize)
352            } else {
353                // otherwise directly shift lhs to required position
354                /* Before:
355                 * lhs: |==========|
356                 * rhs:       |==============|
357                 *                 |< ediff >|
358                 *      |<------ precision ------>|
359                 *
360                 * After:
361                 * lhs: |==========0000000000|
362                 * rhs:       |==============|
363                 */
364                shl_digits_in_place::<B>(&mut lhs.significand, ediff);
365                low = (IBig::ZERO, 0);
366                match rhs_sign {
367                    Positive => (lhs.significand + &rhs.significand, rhs.exponent),
368                    Negative => (lhs.significand - &rhs.significand, rhs.exponent),
369                }
370            };
371
372        self.repr_round_sum(significand, exponent, low, is_sub)
373    }
374
375    // lhs + rhs_sign * rhs, assuming lhs.exponent <= rhs.exponent
376    pub(crate) fn repr_add_small_large<const B: Word>(
377        &self,
378        lhs: Repr<B>,
379        rhs: &Repr<B>,
380        rhs_sign: Sign,
381    ) -> Rounded<Repr<B>> {
382        debug_assert!(lhs.exponent <= rhs.exponent);
383
384        // the following implementation should be exactly the same as `repr_add_large_small`
385        // other than lhs and rhs are swapped. See `repr_add_large_small` for full documentation
386        let is_sub = lhs.significand.sign() != rhs_sign * rhs.significand.sign();
387        let rnd_precision = self.precision + is_sub as usize;
388
389        let ediff = (rhs.exponent - lhs.exponent) as usize;
390        let rdigits = rhs.digits();
391        let ldigits_est = lhs.digits_ub();
392
393        // align the exponent
394        let low: (IBig, usize);
395        let (significand, exponent) =
396            if self.is_limited() && is_sub && ldigits_est + self.precision >= rdigits + ediff {
397                // Symmetric counterpart of the guard in `repr_add_large_small` (see there
398                // for the rationale); here the lower-exponent operand is `lhs`. Form the
399                // exact difference at full operand width and let the shared
400                // `repr_round_sum` round it once.
401                let rhs_signif = shl_digits::<B>(&rhs.significand, ediff);
402                low = (IBig::ZERO, 0);
403                (rhs_sign * rhs_signif + lhs.significand, lhs.exponent)
404            } else if self.is_limited()
405                && ldigits_est + 1 < ediff
406                && ldigits_est + 1 + rnd_precision < rdigits + ediff
407            {
408                // lhs is entirely below rhs's rounding window, so only its sign
409                // contributes; replace it with a unit sticky tail positioned at lhs's
410                // real magnitude (`ediff` digits below rhs's exponent). See
411                // `repr_add_large_small` for why the position must be `ediff` and not
412                // `precision - rdigits`.
413                low = (lhs.significand.signum(), ediff);
414                (rhs_sign * rhs.significand.clone(), rhs.exponent)
415            } else if self.is_limited() && rdigits >= self.precision {
416                // if the rhs already exceeds the desired precision, just align lhs
417                let (lhs_signif, r) = split_digits::<B>(lhs.significand, ediff);
418                low = (r, ediff);
419                match rhs_sign {
420                    Positive => (lhs_signif + &rhs.significand, rhs.exponent),
421                    Negative => (lhs_signif - &rhs.significand, rhs.exponent),
422                }
423            } else if self.is_limited() && ediff + rdigits > self.precision {
424                // if the shifted rhs exceeds the desired precision, align lhs and rhs to precision
425                let lshift = self.precision - rdigits;
426                let rshift = ediff - lshift;
427                let (lhs_signif, r) = split_digits::<B>(lhs.significand, rshift);
428                let rhs_signif = shl_digits::<B>(&rhs.significand, lshift);
429
430                low = (r, rshift);
431                (rhs_sign * rhs_signif + lhs_signif, rhs.exponent - lshift as isize)
432            } else {
433                // otherwise directly shift rhs to required position
434                let rhs_signif = shl_digits::<B>(&rhs.significand, ediff);
435                low = (IBig::ZERO, 0);
436                (rhs_sign * rhs_signif + lhs.significand, lhs.exponent)
437            };
438
439        self.repr_round_sum(significand, exponent, low, is_sub)
440    }
441
442    /// Add or subtract two finite floats, consuming both operands.
443    ///
444    /// Computes `lhs + rhs_sign · rhs`: [`Sign::Positive`] adds, [`Sign::Negative`]
445    /// subtracts. This is the low-level ownership-aware kernel shared by the
446    /// `+`/`-` operators and [`add`](Self::add)/[`sub`](Self::sub); unlike those it
447    /// returns the raw rounded [`Repr`] (no [`FBig`] wrapping, no [`Result`]) and
448    /// reuses the owned significand buffer of `lhs`. The `_vr`/`_rv`/`_rr`
449    /// siblings cover the other ownership combinations and share this contract.
450    ///
451    /// # Panics
452    ///
453    /// Panics if either operand is infinite (matching the other `repr_*` kernels).
454    ///
455    /// # Examples
456    ///
457    /// ```
458    /// use dashu_base::Sign::*;
459    /// use dashu_float::{Context, Repr, round::mode::HalfEven};
460    /// use dashu_int::IBig;
461    ///
462    /// let ctx = Context::<HalfEven>::new(4);
463    /// let a = Repr::<10>::new(IBig::from(1234), -3); // 1.234
464    /// let b = Repr::<10>::new(IBig::from(5678), -4); // 0.5678
465    /// // 1.234 + 0.5678 = 1.8018, rounds (HalfEven, prec 4) to 1.802
466    /// assert_eq!(ctx.addsub_vv(a.clone(), b.clone(), Positive).value(),
467    ///            Repr::<10>::new(IBig::from(1802), -3));
468    /// // 1.234 - 0.5678 = 0.6662 (exact at prec 4)
469    /// assert_eq!(ctx.addsub_vv(a, b, Negative).value(),
470    ///            Repr::<10>::new(IBig::from(6662), -4));
471    /// ```
472    pub fn addsub_vv<const B: Word>(
473        &self,
474        lhs: Repr<B>,
475        rhs: Repr<B>,
476        rhs_sign: Sign,
477    ) -> Rounded<Repr<B>> {
478        assert_finite_operands(&lhs, &rhs);
479        // The zero tests are on the *significand*: `-0` reaches this kernel too (as a value, an
480        // addend, a difference, a cancellation), and treating only `+0` as zero sent `-0` down
481        // the alignment path below — where the sentinel exponent `-1` makes it look like the
482        // *larger* operand, so the other addend's significand is shifted past its end and
483        // dropped (`1e-16 + (-0)` returned `0`).
484        if lhs.significand.is_zero() {
485            if rhs.significand.is_zero() {
486                self.repr_round(zero_sum::<R, B>(
487                    lhs.is_neg_zero(),
488                    rhs.is_neg_zero() ^ (rhs_sign == Negative),
489                ))
490            } else {
491                // With rhs_sign = Negative, round `-rhs` directly rather than negating
492                // *after* rounding. For the asymmetric modes (Up = toward +∞, Down =
493                // toward −∞), `round(-x) != -round(x)`: rounding `rhs` toward +∞ then
494                // negating rounds in the wrong direction, so `0 - rhs` would land one
495                // ULP off (truncated instead of rounded away from the result). This
496                // applies to every `addsub_*` variant's lhs-zero path.
497                self.repr_round(match rhs_sign {
498                    Positive => rhs,
499                    Negative => Repr::new(-rhs.significand, rhs.exponent),
500                })
501            }
502        } else if rhs.significand.is_zero() {
503            self.repr_round(lhs)
504        } else {
505            match lhs.exponent.cmp(&rhs.exponent) {
506                Ordering::Equal => {
507                    let sig = match rhs_sign {
508                        Positive => lhs.significand + &rhs.significand,
509                        Negative => lhs.significand - &rhs.significand,
510                    };
511                    self.repr_round(cancel_zero::<R, B>(sig, lhs.exponent))
512                }
513                Ordering::Greater => self.repr_add_large_small(lhs, &rhs, rhs_sign),
514                Ordering::Less => self.repr_add_small_large(lhs, &rhs, rhs_sign),
515            }
516        }
517    }
518
519    /// Like [`addsub_vv`](Self::addsub_vv) but with the right operand borrowed
520    /// (the left operand is consumed). See `addsub_vv` for the full contract.
521    pub fn addsub_vr<const B: Word>(
522        &self,
523        lhs: Repr<B>,
524        rhs: &Repr<B>,
525        rhs_sign: Sign,
526    ) -> Rounded<Repr<B>> {
527        assert_finite_operands(&lhs, rhs);
528        // Significand-based zero tests — see `addsub_vv`.
529        if lhs.significand.is_zero() {
530            if rhs.significand.is_zero() {
531                self.repr_round(zero_sum::<R, B>(
532                    lhs.is_neg_zero(),
533                    rhs.is_neg_zero() ^ (rhs_sign == Negative),
534                ))
535            } else {
536                self.repr_round(match rhs_sign {
537                    Positive => rhs.clone(),
538                    Negative => Repr::new(-&rhs.significand, rhs.exponent),
539                })
540            }
541        } else if rhs.significand.is_zero() {
542            self.repr_round(lhs)
543        } else {
544            match lhs.exponent.cmp(&rhs.exponent) {
545                Ordering::Equal => {
546                    let sig = match rhs_sign {
547                        Positive => lhs.significand + &rhs.significand,
548                        Negative => lhs.significand - &rhs.significand,
549                    };
550                    self.repr_round(cancel_zero::<R, B>(sig, lhs.exponent))
551                }
552                Ordering::Greater => self.repr_add_large_small(lhs, rhs, rhs_sign),
553                Ordering::Less => self.repr_add_small_large(lhs, rhs, rhs_sign),
554            }
555        }
556    }
557
558    /// Like [`addsub_vv`](Self::addsub_vv) but with the left operand borrowed and
559    /// the right operand consumed. See `addsub_vv` for the full contract.
560    pub fn addsub_rv<const B: Word>(
561        &self,
562        lhs: &Repr<B>,
563        mut rhs: Repr<B>,
564        rhs_sign: Sign,
565    ) -> Rounded<Repr<B>> {
566        assert_finite_operands(lhs, &rhs);
567        // Bake the sign into the owned rhs so the kernel — which takes its first
568        // operand by value — can move `rhs` into that slot with `Positive`. This
569        // is the mirror of `addsub_vv`'s owned-lhs path; the value is identical.
570        // (The zero-sign test below is taken first: negating a zero significand leaves the
571        // `-0` sentinel exponent untouched, so `rhs.is_neg_zero()` would lie after the bake.)
572        let rhs_eff_neg = rhs.is_neg_zero() ^ (rhs_sign == Negative);
573        rhs.significand *= rhs_sign;
574        // Significand-based zero tests — see `addsub_vv`.
575        if lhs.significand.is_zero() {
576            if rhs.significand.is_zero() {
577                self.repr_round(zero_sum::<R, B>(lhs.is_neg_zero(), rhs_eff_neg))
578            } else {
579                self.repr_round(rhs)
580            }
581        } else if rhs.significand.is_zero() {
582            self.repr_round_ref(lhs)
583        } else {
584            match lhs.exponent.cmp(&rhs.exponent) {
585                Ordering::Equal => self.repr_round(cancel_zero::<R, B>(
586                    &lhs.significand + rhs.significand,
587                    lhs.exponent,
588                )),
589                Ordering::Greater => self.repr_add_small_large(rhs, lhs, Positive),
590                Ordering::Less => self.repr_add_large_small(rhs, lhs, Positive),
591            }
592        }
593    }
594
595    /// Like [`addsub_vv`](Self::addsub_vv) but with both operands borrowed (the
596    /// larger-exponent operand is cloned once for in-place alignment). See
597    /// `addsub_vv` for the full contract.
598    pub fn addsub_rr<const B: Word>(
599        &self,
600        lhs: &Repr<B>,
601        rhs: &Repr<B>,
602        rhs_sign: Sign,
603    ) -> Rounded<Repr<B>> {
604        assert_finite_operands(lhs, rhs);
605        // Significand-based zero tests — see `addsub_vv`.
606        if lhs.significand.is_zero() {
607            if rhs.significand.is_zero() {
608                self.repr_round(zero_sum::<R, B>(
609                    lhs.is_neg_zero(),
610                    rhs.is_neg_zero() ^ (rhs_sign == Negative),
611                ))
612            } else {
613                match rhs_sign {
614                    Positive => self.repr_round_ref(rhs),
615                    Negative => self.repr_round_ref(&Repr::new(-&rhs.significand, rhs.exponent)),
616                }
617            }
618        } else if rhs.significand.is_zero() {
619            self.repr_round_ref(lhs)
620        } else {
621            match lhs.exponent.cmp(&rhs.exponent) {
622                Ordering::Equal => {
623                    let sig = match rhs_sign {
624                        Positive => &lhs.significand + &rhs.significand,
625                        Negative => &lhs.significand - &rhs.significand,
626                    };
627                    self.repr_round(cancel_zero::<R, B>(sig, lhs.exponent))
628                }
629                Ordering::Greater => self.repr_add_large_small(lhs.clone(), rhs, rhs_sign),
630                Ordering::Less => self.repr_add_small_large(lhs.clone(), rhs, rhs_sign),
631            }
632        }
633    }
634
635    /// Add two floating point numbers under this context.
636    ///
637    /// # Examples
638    ///
639    /// ```
640    /// # use core::str::FromStr;
641    /// # use dashu_base::ParseError;
642    /// # use dashu_float::DBig;
643    /// use dashu_base::Approximation::*;
644    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
645    ///
646    /// let context = Context::<HalfAway>::new(2);
647    /// let a = DBig::from_str("1.234")?;
648    /// let b = DBig::from_str("6.789")?;
649    /// assert_eq!(context.add(&a.repr(), &b.repr()), Ok(Inexact(DBig::from_str("8.0")?, NoOp)));
650    /// # Ok::<(), ParseError>(())
651    /// ```
652    #[inline]
653    pub fn add<const B: Word>(&self, lhs: &Repr<B>, rhs: &Repr<B>) -> FpResult<FBig<R, B>> {
654        if lhs.is_infinite() || rhs.is_infinite() {
655            return Err(FpError::InfiniteInput);
656        }
657        Ok(self
658            .addsub_rr(lhs, rhs, Positive)
659            .map(|v| FBig::new(v, *self)))
660    }
661
662    /// Subtract two floating point numbers under this context.
663    ///
664    /// # Examples
665    ///
666    /// ```
667    /// # use core::str::FromStr;
668    /// # use dashu_base::ParseError;
669    /// # use dashu_float::DBig;
670    /// use dashu_base::Approximation::*;
671    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
672    ///
673    /// let context = Context::<HalfAway>::new(2);
674    /// let a = DBig::from_str("1.234")?;
675    /// let b = DBig::from_str("6.789")?;
676    /// assert_eq!(
677    ///     context.sub(&a.repr(), &b.repr()),
678    ///     Ok(Inexact(DBig::from_str("-5.6")?, SubOne))
679    /// );
680    /// # Ok::<(), ParseError>(())
681    /// ```
682    #[inline]
683    pub fn sub<const B: Word>(&self, lhs: &Repr<B>, rhs: &Repr<B>) -> FpResult<FBig<R, B>> {
684        if lhs.is_infinite() || rhs.is_infinite() {
685            return Err(FpError::InfiniteInput);
686        }
687        Ok(self
688            .addsub_rr(lhs, rhs, Negative)
689            .map(|v| FBig::new(v, *self)))
690    }
691}
692
693#[cfg(test)]
694mod tests {
695    use super::*;
696    use crate::round::mode::{self, HalfAway, HalfEven};
697
698    // Build a normalized Repr from a small integer significand and an exponent.
699    fn r<const B: Word>(sig: i128, exp: isize) -> Repr<B> {
700        Repr::new(IBig::from(sig), exp)
701    }
702
703    // Severe cancellation must not collapse a genuinely small difference to 0.
704    // Pristine returned `0` for the first two rows: the trimmed alignment path
705    // keeps only a bounded low tail and its single re-expand can't recover the
706    // lost leading digits.
707    #[test]
708    fn sub_severe_cancellation_decimal() {
709        let ctx = Context::<HalfAway>::new(3);
710        // 1.00 - 0.99999999 = 1e-8 (exactly representable at precision 3)
711        assert_eq!(
712            ctx.sub(&r::<10>(100, -2), &r::<10>(99999999, -8))
713                .unwrap()
714                .value()
715                .repr(),
716            &r::<10>(1, -8)
717        );
718        // 1.00 - 0.99950001 = 4.9999e-4, rounds to 5.00e-4 (HalfAway)
719        assert_eq!(
720            ctx.sub(&r::<10>(100, -2), &r::<10>(99950001, -8))
721                .unwrap()
722                .value()
723                .repr(),
724            &r::<10>(500, -6)
725        );
726    }
727
728    #[test]
729    fn sub_severe_cancellation_binary() {
730        let ctx = Context::<HalfEven>::new(10);
731        // 2^20 - (2^20 - 1) = 1, with the operands 20 exponent positions apart
732        assert_eq!(
733            ctx.sub(&r::<2>(1, 20), &r::<2>((1i128 << 20) - 1, 0))
734                .unwrap()
735                .value()
736                .repr(),
737            &r::<2>(1, 0)
738        );
739        // same magnitude gap but the smaller-exponent operand is on the left
740        assert_eq!(
741            ctx.sub(&r::<2>((1i128 << 20) - 1, 0), &r::<2>(1, 20))
742                .unwrap()
743                .value()
744                .repr(),
745            &r::<2>(-1, 0)
746        );
747        // 2^30 - (2^30 - 1) = 1
748        assert_eq!(
749            ctx.sub(&r::<2>(1, 30), &r::<2>((1i128 << 30) - 1, 0))
750                .unwrap()
751                .value()
752                .repr(),
753            &r::<2>(1, 0)
754        );
755    }
756
757    // Effective subtraction reached through `Context::add` (opposite signs) must
758    // be fixed as well.
759    #[test]
760    fn add_effective_severe_cancellation() {
761        let ctx = Context::<HalfEven>::new(10);
762        // 2^20 + (-(2^20 - 1)) = 1
763        assert_eq!(
764            ctx.add(&r::<2>(1, 20), &r::<2>(-((1i128 << 20) - 1), 0))
765                .unwrap()
766                .value()
767                .repr(),
768            &r::<2>(1, 0)
769        );
770    }
771
772    // The public operator path (`a - b`) routes through the same kernel.
773    #[test]
774    fn sub_operator_severe_cancellation() {
775        let a = FBig::<HalfEven, 2>::from_parts(IBig::from(1), 20);
776        let b = FBig::<HalfEven, 2>::from_parts(IBig::from((1i128 << 20) - 1), 0);
777        assert_eq!((a - b).repr(), &r::<2>(1, 0));
778    }
779
780    // Mild subtractions — the smaller operand stays below the larger's precision
781    // window — must keep their existing behavior and not be diverted to the
782    // full-width path.
783    #[test]
784    fn sub_mild_unchanged() {
785        let ctx = Context::<HalfAway>::new(3);
786        // 101 - 0.2 = 100.8, kept as 1008 * 10^-1 (one guard digit, as before)
787        assert_eq!(
788            ctx.sub(&r::<10>(101, 0), &r::<10>(2, -1))
789                .unwrap()
790                .value()
791                .repr(),
792            &r::<10>(1008, -1)
793        );
794    }
795
796    // Regression for the branch-1 signum-proxy bug (SUM-BUG.md §2c): when the larger
797    // operand has fewer digits than the precision and a negligible operand is added,
798    // the sticky proxy must be positioned at the operand's *real* magnitude (`ediff`),
799    // not at `precision - ldigits`. The old positioning let the re-expand drag the
800    // sticky up to the LSB, where for base 2 + round-to-nearest it equals exactly half
801    // and injected a spurious ULP: `1 + 2^-100` at precision 10 gave `513*2^-9` (=
802    // 1.00195…) instead of `1`.
803    #[test]
804    fn add_negligible_short_operand_no_spurious_ulp() {
805        // base 2 + HalfAway: the exact tie case
806        let ctx = Context::<HalfAway>::new(10);
807        assert_eq!(
808            ctx.add(&r::<2>(1, 0), &r::<2>(1, -100))
809                .unwrap()
810                .value()
811                .repr(),
812            &r::<2>(1, 0)
813        );
814        assert_eq!(
815            ctx.sub(&r::<2>(1, 0), &r::<2>(1, -100))
816                .unwrap()
817                .value()
818                .repr(),
819            &r::<2>(1, 0)
820        );
821        // larger short operand (digits < precision), negligible addend
822        let ctx = Context::<HalfAway>::new(50);
823        assert_eq!(
824            ctx.add(&r::<2>(0x12345, 0), &r::<2>(1, -200))
825                .unwrap()
826                .value()
827                .repr(),
828            &r::<2>(0x12345, 0)
829        );
830        // base 10 was never affected (1 < ½·10), but check it stays correct
831        let ctx = Context::<HalfAway>::new(10);
832        assert_eq!(
833            ctx.add(&r::<10>(1, 0), &r::<10>(1, -100))
834                .unwrap()
835                .value()
836                .repr(),
837            &r::<10>(1, 0)
838        );
839    }
840
841    // The four ownership-aware `addsub_*` variants must produce the same value as
842    // each other (and therefore as `Context::add`/`sub`, which delegate to
843    // `addsub_rr`) across every input shape. The absolute correctness of
844    // `addsub_rr` is covered by the `ctx.add`/`ctx.sub` tests above; this guards
845    // that consuming vs borrowing an operand never changes the result.
846    fn assert_addsub_variants_agree<R: Round, const B: Word>(
847        ctx: &Context<R>,
848        a: &Repr<B>,
849        b: &Repr<B>,
850    ) {
851        let add = ctx.addsub_rr(a, b, Positive).value();
852        let sub = ctx.addsub_rr(a, b, Negative).value();
853        assert_eq!(&ctx.addsub_vv(a.clone(), b.clone(), Positive).value(), &add);
854        assert_eq!(&ctx.addsub_vr(a.clone(), b, Positive).value(), &add);
855        assert_eq!(&ctx.addsub_rv(a, b.clone(), Positive).value(), &add);
856        assert_eq!(&ctx.addsub_vv(a.clone(), b.clone(), Negative).value(), &sub);
857        assert_eq!(&ctx.addsub_vr(a.clone(), b, Negative).value(), &sub);
858        assert_eq!(&ctx.addsub_rv(a, b.clone(), Negative).value(), &sub);
859    }
860
861    #[test]
862    fn addsub_variants_agree() {
863        let ctx = Context::<HalfAway>::new(3);
864        // lhs has the larger exponent
865        assert_addsub_variants_agree(&ctx, &r::<10>(1234, -2), &r::<10>(567, -5));
866        // rhs has the larger exponent
867        assert_addsub_variants_agree(&ctx, &r::<10>(567, -5), &r::<10>(1234, -2));
868        // equal exponents
869        assert_addsub_variants_agree(&ctx, &r::<10>(123, -2), &r::<10>(456, -2));
870        // lhs is +0 (exercises the lhs-zero short-circuit in every variant)
871        assert_addsub_variants_agree(&ctx, &r::<10>(0, 0), &r::<10>(123, -2));
872        // rhs is +0
873        assert_addsub_variants_agree(&ctx, &r::<10>(123, -2), &r::<10>(0, 0));
874        // a negative operand (effective subtraction reached through `add`)
875        assert_addsub_variants_agree(&ctx, &r::<10>(-123, -2), &r::<10>(456, -2));
876    }
877
878    // Severe cancellation must recover through every ownership path — especially
879    // `addsub_rv`, which bakes the sign into the owned rhs and routes it as the
880    // kernel's by-value (first) operand, a structurally different path from
881    // vv/vr/rr.
882    #[test]
883    fn addsub_severe_cancellation_all_variants() {
884        let ctx = Context::<HalfEven>::new(10);
885        let big = r::<2>(1, 20); // 2^20
886        let near = r::<2>((1i128 << 20) - 1, 0); // 2^20 - 1
887                                                 // 2^20 - (2^20 - 1) = 1
888        assert_eq!(&ctx.addsub_vv(big.clone(), near.clone(), Negative).value(), &r::<2>(1, 0));
889        assert_eq!(&ctx.addsub_vr(big.clone(), &near, Negative).value(), &r::<2>(1, 0));
890        assert_eq!(&ctx.addsub_rr(&big, &near, Negative).value(), &r::<2>(1, 0));
891        // reversed: (2^20 - 1) - 2^20 = -1, through both rhs-owned and lhs-owned paths
892        assert_eq!(&ctx.addsub_rv(&near, big.clone(), Negative).value(), &r::<2>(-1, 0));
893        assert_eq!(&ctx.addsub_vv(near, big, Negative).value(), &r::<2>(-1, 0));
894    }
895
896    /// `x + (-0) == x` (and the same through `sub`), for a magnitude small enough that the
897    /// exponent gap to `-0`'s sentinel exponent reaches past the end of `x`'s significand.
898    ///
899    /// Regression: the kernels tested only `is_pos_zero()` for their zero short-circuit, so
900    /// `-0` fell into the alignment path — where its sentinel exponent `-1` makes it look like
901    /// the *larger* operand, shifting `x`'s significand out entirely. `1e-16 + (-0)` returned
902    /// `0`, and the same for every `|x|` whose significand is shorter than the gap.
903    #[test]
904    fn addsub_neg_zero_is_identity_for_tiny_operands() {
905        fn check<R: Round>(name: &str) {
906            let ctx = Context::<R>::new(12);
907            for (sig, exp) in [(1i128, -16), (1, -30), (12345, -40), (7, -3), (5, -2)] {
908                let x = r::<10>(sig, exp);
909                let neg_zero = Repr::<10>::neg_zero();
910                let expected = r::<10>(sig, exp);
911                assert_eq!(
912                    ctx.addsub_rr(&x, &neg_zero, Positive).value(),
913                    expected,
914                    "{name}: {sig}e{exp} + (-0)"
915                );
916                assert_eq!(
917                    ctx.addsub_rr(&neg_zero, &x, Positive).value(),
918                    expected,
919                    "{name}: (-0) + {sig}e{exp}"
920                );
921                // `x - (+0)` and `x - (-0)` are the same identity
922                assert_eq!(
923                    ctx.addsub_rr(&x, &Repr::<10>::zero(), Negative).value(),
924                    expected,
925                    "{name}: {sig}e{exp} - (+0)"
926                );
927                assert_eq!(
928                    ctx.addsub_rr(&x, &neg_zero, Negative).value(),
929                    expected,
930                    "{name}: {sig}e{exp} - (-0)"
931                );
932                assert_addsub_variants_agree(&ctx, &x, &neg_zero);
933            }
934        }
935        check::<HalfAway>("HalfAway");
936        check::<HalfEven>("HalfEven");
937        check::<mode::Down>("Down");
938        check::<mode::Up>("Up");
939        check::<mode::Zero>("Zero");
940    }
941
942    /// IEEE 754 §6.3 conformance for a sum/difference whose *both* operands are zero: the sign
943    /// of the exact zero. `x + x` (and `x − (−x)`) retains `x`'s sign even when `x` is zero;
944    /// an exact zero of mixed signs is `+0`, except under roundTowardNegative where it is `-0`.
945    ///
946    /// Written as the explicit table rather than re-deriving the rule, so the test is an
947    /// independent restatement of the standard.
948    #[test]
949    fn addsub_signed_zero_results_match_ieee754() {
950        // (lhs is -0, rhs is -0, sign of `lhs + rhs`, sign of `lhs - rhs`) under the ordinary
951        // rounding modes, and the same under roundTowardNegative.
952        let cases: [(bool, bool, bool, bool, bool, bool); 4] = [
953            //  lhs-0  rhs-0 | add/normal  sub/normal | add/rtn  sub/rtn
954            (true, true, true, false, true, true),
955            (true, false, false, true, true, true),
956            // `(+0) − (−0)` is `(+0) + (+0)`: the like-sign clause keeps it `+0` under *every*
957            // mode, roundTowardNegative included.
958            (false, true, false, false, true, false),
959            (false, false, false, false, false, true),
960        ];
961
962        fn check<R: Round, const B: Word>(
963            name: &str,
964            cases: &[(bool, bool, bool, bool, bool, bool)],
965        ) {
966            let ctx = Context::<R>::new(5);
967            for &(lhs_neg, rhs_neg, add_neg, sub_neg, add_rtn, sub_rtn) in cases {
968                let z = |neg: bool| {
969                    if neg {
970                        Repr::<B>::neg_zero()
971                    } else {
972                        Repr::<B>::zero()
973                    }
974                };
975                let (lhs, rhs) = (z(lhs_neg), z(rhs_neg));
976
977                let add = ctx.addsub_rr(&lhs, &rhs, Positive).value();
978                let neg = R::IS_ROUND_TOWARD_NEGATIVE;
979                let want_add_neg = if neg { add_rtn } else { add_neg };
980                assert_eq!(
981                    add.is_neg_zero(),
982                    want_add_neg,
983                    "{name}: (-0={lhs_neg}) + (-0={rhs_neg})"
984                );
985                assert!(add.significand.is_zero());
986
987                let sub = ctx.addsub_rr(&lhs, &rhs, Negative).value();
988                let want_sub_neg = if neg { sub_rtn } else { sub_neg };
989                assert_eq!(
990                    sub.is_neg_zero(),
991                    want_sub_neg,
992                    "{name}: (-0={lhs_neg}) - (-0={rhs_neg})"
993                );
994                assert!(sub.significand.is_zero());
995
996                // every ownership path agrees on the signed zero
997                assert_addsub_variants_agree(&ctx, &lhs, &rhs);
998            }
999        }
1000
1001        check::<HalfAway, 10>("HalfAway", &cases);
1002        check::<HalfEven, 10>("HalfEven", &cases);
1003        check::<mode::Up, 10>("Up", &cases);
1004        check::<mode::Zero, 10>("Zero", &cases);
1005        check::<mode::Down, 10>("Down", &cases); // roundTowardNegative
1006        check::<HalfEven, 2>("HalfEven base 2", &cases);
1007    }
1008
1009    // An astronomically large exponent gap (here ~10⁹ digits) must not try to materialize the
1010    // digit alignment: the smaller operand is a pure sticky below the rounding window, and the
1011    // result is just the larger operand (rounded to nearest). Before the sticky collapse in
1012    // `repr_round_sum`, base-10 inputs here died on an out-of-memory allocation (e.g. via
1013    // `sinh(1e14)`, whose two exponentials sit ~10¹⁵ digits apart).
1014    #[test]
1015    fn addsub_astronomical_exponent_gap() {
1016        let ctx = Context::<HalfEven>::new(20);
1017        let big = r::<10>(12345, 1_000_000_000);
1018        let tiny = r::<10>(54321, -1_000_000_000);
1019        // big + tiny ≈ big (the tiny term is far below the window, but keeps the sum inexact:
1020        // both round-to-nearest and the directed modes round back to big itself)
1021        let sum = ctx.add(&big, &tiny).unwrap().value();
1022        assert_eq!(sum.repr().exponent(), big.exponent);
1023        // big - tiny likewise
1024        let diff = ctx.sub(&big, &tiny).unwrap().value();
1025        assert_eq!(diff.repr().exponent(), big.exponent);
1026        // tiny - big ≈ -big
1027        let neg = ctx.sub(&tiny, &big).unwrap().value();
1028        assert_eq!(neg.repr().sign(), dashu_base::Sign::Negative);
1029        assert_eq!(neg.repr().exponent(), big.exponent);
1030    }
1031}