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_int::{IBig, UBig};
16
17/// Build a `Repr` from a cancellation result, producing `-0` (instead of `+0`) when the
18/// significand is zero and the rounding mode is roundTowardNegative (IEEE 754 §6.3).
19fn cancel_zero<R: Round, const B: Word>(sig: IBig, exp: isize) -> Repr<B> {
20    if sig.is_zero() && R::IS_ROUND_TOWARD_NEGATIVE {
21        Repr::neg_zero()
22    } else {
23        Repr::new(sig, exp)
24    }
25}
26
27impl<R: Round, const B: Word> Add for FBig<R, B> {
28    type Output = Self;
29
30    #[inline]
31    fn add(self, rhs: Self) -> Self::Output {
32        add_val_val(self, rhs, Positive)
33    }
34}
35
36impl<R: Round, const B: Word> Add<&FBig<R, B>> for FBig<R, B> {
37    type Output = Self;
38
39    #[inline]
40    fn add(self, rhs: &FBig<R, B>) -> Self::Output {
41        add_val_ref(self, rhs, Positive)
42    }
43}
44
45impl<R: Round, const B: Word> Add<FBig<R, B>> for &FBig<R, B> {
46    type Output = FBig<R, B>;
47
48    #[inline]
49    fn add(self, rhs: FBig<R, B>) -> Self::Output {
50        add_ref_val(self, rhs, Positive)
51    }
52}
53
54impl<R: Round, const B: Word> Add<&FBig<R, B>> for &FBig<R, B> {
55    type Output = FBig<R, B>;
56
57    #[inline]
58    fn add(self, rhs: &FBig<R, B>) -> Self::Output {
59        add_ref_ref(self, rhs, Positive)
60    }
61}
62
63impl<R: Round, const B: Word> Sub for FBig<R, B> {
64    type Output = Self;
65
66    #[inline]
67    fn sub(self, rhs: Self) -> Self::Output {
68        add_val_val(self, rhs, Negative)
69    }
70}
71
72impl<R: Round, const B: Word> Sub<&FBig<R, B>> for FBig<R, B> {
73    type Output = Self;
74
75    #[inline]
76    fn sub(self, rhs: &FBig<R, B>) -> Self::Output {
77        add_val_ref(self, rhs, Negative)
78    }
79}
80
81impl<R: Round, const B: Word> Sub<FBig<R, B>> for &FBig<R, B> {
82    type Output = FBig<R, B>;
83
84    #[inline]
85    fn sub(self, rhs: FBig<R, B>) -> Self::Output {
86        add_ref_val(self, rhs, Negative)
87    }
88}
89
90impl<R: Round, const B: Word> Sub<&FBig<R, B>> for &FBig<R, B> {
91    type Output = FBig<R, B>;
92
93    #[inline]
94    fn sub(self, rhs: &FBig<R, B>) -> Self::Output {
95        add_ref_ref(self, rhs, Negative)
96    }
97}
98
99helper_macros::impl_binop_assign_by_taking!(impl AddAssign<Self>, add_assign, add);
100helper_macros::impl_binop_assign_by_taking!(impl SubAssign<Self>, sub_assign, sub);
101
102macro_rules! impl_add_sub_primitive_with_fbig {
103    ($($t:ty)*) => {$(
104        helper_macros::impl_binop_with_primitive!(impl Add<$t>, add);
105        helper_macros::impl_binop_assign_with_primitive!(impl AddAssign<$t>, add_assign);
106        helper_macros::impl_binop_with_primitive!(impl Sub<$t>, sub);
107        helper_macros::impl_binop_assign_with_primitive!(impl SubAssign<$t>, sub_assign);
108    )*};
109}
110impl_add_sub_primitive_with_fbig!(u8 u16 u32 u64 u128 usize UBig i8 i16 i32 i64 i128 isize IBig);
111
112fn add_val_val<R: Round, const B: Word>(
113    lhs: FBig<R, B>,
114    mut rhs: FBig<R, B>,
115    rhs_sign: Sign,
116) -> FBig<R, B> {
117    assert_finite_operands(&lhs.repr, &rhs.repr);
118
119    let context = Context::max(lhs.context, rhs.context);
120    rhs.repr.significand *= rhs_sign;
121    let sum = if lhs.repr.is_pos_zero() {
122        rhs.repr
123    } else if rhs.repr.is_pos_zero() {
124        lhs.repr
125    } else {
126        match lhs.repr.exponent.cmp(&rhs.repr.exponent) {
127            Ordering::Equal => context.repr_round(cancel_zero::<R, B>(
128                lhs.repr.significand + rhs.repr.significand,
129                lhs.repr.exponent,
130            )),
131            Ordering::Greater => context.repr_add_large_small(lhs.repr, &rhs.repr, Positive),
132            Ordering::Less => context.repr_add_small_large(lhs.repr, &rhs.repr, Positive),
133        }
134        .value()
135    };
136    FBig::new(sum, context)
137}
138
139fn add_val_ref<R: Round, const B: Word>(
140    lhs: FBig<R, B>,
141    rhs: &FBig<R, B>,
142    rhs_sign: Sign,
143) -> FBig<R, B> {
144    assert_finite_operands(&lhs.repr, &rhs.repr);
145
146    let context = Context::max(lhs.context, rhs.context);
147    let sum = if lhs.repr.is_pos_zero() {
148        let mut repr = rhs.repr.clone();
149        repr.significand *= rhs_sign;
150        repr
151    } else if rhs.repr.is_pos_zero() {
152        lhs.repr
153    } else {
154        match lhs.repr.exponent.cmp(&rhs.repr.exponent) {
155            Ordering::Equal => {
156                let sum_signif = match rhs_sign {
157                    Positive => lhs.repr.significand + &rhs.repr.significand,
158                    Negative => lhs.repr.significand - &rhs.repr.significand,
159                };
160                context.repr_round(cancel_zero::<R, B>(sum_signif, lhs.repr.exponent))
161            }
162            Ordering::Greater => context.repr_add_large_small(lhs.repr, &rhs.repr, rhs_sign),
163            Ordering::Less => context.repr_add_small_large(lhs.repr, &rhs.repr, rhs_sign),
164        }
165        .value()
166    };
167    FBig::new(sum, context)
168}
169
170fn add_ref_val<R: Round, const B: Word>(
171    lhs: &FBig<R, B>,
172    mut rhs: FBig<R, B>,
173    rhs_sign: Sign,
174) -> FBig<R, B> {
175    assert_finite_operands(&lhs.repr, &rhs.repr);
176
177    let context = Context::max(lhs.context, rhs.context);
178    rhs.repr.significand *= rhs_sign;
179    let sum = if lhs.repr.is_pos_zero() {
180        rhs.repr
181    } else if rhs.repr.is_pos_zero() {
182        lhs.repr.clone()
183    } else {
184        match lhs.repr.exponent.cmp(&rhs.repr.exponent) {
185            Ordering::Equal => context.repr_round(cancel_zero::<R, B>(
186                &lhs.repr.significand + rhs.repr.significand,
187                lhs.repr.exponent,
188            )),
189            Ordering::Greater => context.repr_add_small_large(rhs.repr, &lhs.repr, Positive),
190            Ordering::Less => context.repr_add_large_small(rhs.repr, &lhs.repr, Positive),
191        }
192        .value()
193    };
194    FBig::new(sum, context)
195}
196
197fn add_ref_ref<R: Round, const B: Word>(
198    lhs: &FBig<R, B>,
199    rhs: &FBig<R, B>,
200    rhs_sign: Sign,
201) -> FBig<R, B> {
202    assert_finite_operands(&lhs.repr, &rhs.repr);
203
204    let context = Context::max(lhs.context, rhs.context);
205    let sum = if lhs.repr.is_pos_zero() {
206        let mut repr = rhs.repr.clone();
207        repr.significand *= rhs_sign;
208        repr
209    } else if rhs.repr.is_pos_zero() {
210        lhs.repr.clone()
211    } else {
212        match lhs.repr.exponent.cmp(&rhs.repr.exponent) {
213            Ordering::Equal => context.repr_round(cancel_zero::<R, B>(
214                &lhs.repr.significand + rhs_sign * rhs.repr.significand.clone(),
215                lhs.repr.exponent,
216            )),
217            Ordering::Greater => {
218                context.repr_add_large_small(lhs.repr.clone(), &rhs.repr, rhs_sign)
219            }
220            Ordering::Less => context.repr_add_small_large(lhs.repr.clone(), &rhs.repr, rhs_sign),
221        }
222        .value()
223    };
224    FBig::new(sum, context)
225}
226
227impl<R: Round> Context<R> {
228    /// Round sum = `significand * B ^ exponent` with the low part (value, precision).
229    /// If the sum is actually from a subtraction and the low part is not zero, `is_sub` should be true.
230    fn repr_round_sum<const B: Word>(
231        &self,
232        mut significand: IBig,
233        mut exponent: isize,
234        mut low: (IBig, usize),
235        is_sub: bool,
236    ) -> Rounded<Repr<B>> {
237        // A zero produced by exact cancellation is -0 only under roundTowardNegative (Down),
238        // +0 otherwise (IEEE 754 §6.3).
239        let neg_cancel = is_sub && R::IS_ROUND_TOWARD_NEGATIVE;
240        let make_repr = |sig: IBig, exp: isize| -> Repr<B> {
241            if sig.is_zero() && neg_cancel {
242                Repr::neg_zero()
243            } else {
244                Repr::new(sig, exp)
245            }
246        };
247
248        if !self.is_limited() {
249            // short cut for unlimited precision
250            return Rounded::Exact(make_repr(significand, exponent));
251        }
252
253        // use one extra digit to prevent cancellation in rounding
254        let rnd_precision = self.precision + is_sub as usize;
255
256        // align to precision again
257        let digits = digit_len::<B>(&significand);
258        match digits.cmp(&rnd_precision) {
259            Ordering::Equal => {}
260            Ordering::Greater => {
261                // Shrink if the result has more digits than desired precision
262                /*
263                 * lhs:         |=========0000|
264                 * rhs:              |========|xxxxx|
265                 * sum:        |==============|xxxxx|
266                 * precision:  |<----->|
267                 * shrink:     |=======|xxxxxxxxxxxx|
268                 */
269                let shift = digits - rnd_precision;
270                let (signif_hi, mut signif_lo) = split_digits::<B>(significand, shift);
271                significand = signif_hi;
272                exponent += shift as isize;
273                shl_digits_in_place::<B>(&mut signif_lo, low.1);
274                low.0 += signif_lo;
275                low.1 += shift;
276            }
277            Ordering::Less => {
278                // Expand to low parts if the result has less digits than desired precision.
279                /*
280                 * A possible case when lhs and rhs have different sign:
281                 * lhs:  |=========0000|
282                 * rhs:  |=============|xxxxx|
283                 * sum:          |=====|xxxxx|
284                 * precision+1:  |<------>|
285                 * shift:              |<>|
286                 * expanded:     |========|xx|
287                 */
288                if !low.0.is_zero() {
289                    let (low_val, low_prec) = low;
290                    let shift = low_prec.min(rnd_precision - digits);
291                    let (pad, low_val) = split_digits::<B>(low_val, low_prec - shift);
292                    shl_digits_in_place::<B>(&mut significand, shift);
293                    exponent -= shift as isize;
294                    significand += pad;
295                    low = (low_val, low_prec - shift);
296                }
297            }
298        };
299
300        // perform rounding
301        if low.0.is_zero() {
302            Rounded::Exact(make_repr(significand, exponent))
303        } else {
304            // By now significand should have at least full precision. After adjustment, the digits length
305            // could be one more than the precision. We don't shrink the extra digit.
306            let adjust = R::round_fract::<B>(&significand, low.0, low.1);
307            Rounded::Inexact(make_repr(significand + adjust, exponent), adjust)
308        }
309    }
310
311    // lhs + rhs_sign * rhs, assuming lhs.exponent >= rhs.exponent
312    fn repr_add_large_small<const B: Word>(
313        &self,
314        mut lhs: Repr<B>,
315        rhs: &Repr<B>,
316        rhs_sign: Sign,
317    ) -> Rounded<Repr<B>> {
318        debug_assert!(lhs.exponent >= rhs.exponent);
319
320        // use one extra digit when subtracting to prevent cancellation in rounding
321        let is_sub = lhs.significand.sign() != rhs_sign * rhs.significand.sign();
322        let rnd_precision = self.precision + is_sub as usize;
323
324        let ediff = (lhs.exponent - rhs.exponent) as usize;
325        let ldigits = lhs.digits();
326        let rdigits_est = rhs.digits_ub(); // overestimate
327
328        // align the exponent
329        let low: (IBig, usize); // (value of low part, precision of the low part)
330        let (significand, exponent) =
331            if self.is_limited() && is_sub && rdigits_est + self.precision >= ldigits + ediff {
332                // The smaller operand (`rhs`, lower exponent) reaches the larger
333                // operand's `precision`-digit window — its top digit is at or above the
334                // window edge (`rdigits + precision >= ldigits + ediff`, i.e. `rhs`'s top
335                // position `rdigits - ediff` is `>= ldigits - precision`). An effective
336                // subtraction can then cancel and lose leading digits, which the trimmed
337                // path cannot recover (its single re-expand in `repr_round_sum` collapses
338                // a genuinely small difference to the wrong value — e.g. `1.00 -
339                // 0.99999999` at precision 3 to `0` instead of `1e-8`, or `0.5 - 0.4375`
340                // at precision 1 to `0` instead of `0.0625`). So form the exact difference
341                // at full operand width and let the shared `repr_round_sum` round it once
342                // (with the same guard digit as the trimmed path, so no low tail is
343                // needed). The complement (`<`) is the trimmed/negligible region where
344                // `rhs` stays strictly below the window and no cancellation is possible.
345                shl_digits_in_place::<B>(&mut lhs.significand, ediff);
346                low = (IBig::ZERO, 0);
347                match rhs_sign {
348                    Positive => (lhs.significand + &rhs.significand, rhs.exponent),
349                    Negative => (lhs.significand - &rhs.significand, rhs.exponent),
350                }
351            } else if self.is_limited()
352                && rdigits_est + 1 < ediff
353                && rdigits_est + 1 + rnd_precision < ldigits + ediff
354            {
355                // rhs is entirely below lhs's rounding window, so only its sign
356                // contributes to the rounding; replace it with a unit sticky tail
357                // (`|low| = 1`).
358                //
359                // The sticky must be positioned at rhs's *real* magnitude, i.e. `ediff`
360                // digits below lhs's exponent — NOT at `precision - ldigits`. Positioning
361                // by `ediff` keeps the sticky genuinely sub-ULP (|1| << B^ediff, and the
362                // branch guard guarantees ediff >= 3), so it can never land on a rounding
363                // tie. Positioning by `precision - ldigits` instead let the re-expand drag
364                // the sticky up to the LSB, where for base 2 + round-to-nearest it equals
365                // exactly half (1 == B^0 == ½·B^1) and injected a spurious ULP — e.g.
366                // `1 + 2^-100` at precision 10 returned `513·2^-9` instead of `1`.
367                low = (rhs_sign * rhs.significand.signum(), ediff);
368                (lhs.significand, lhs.exponent)
369            } else if self.is_limited() && ldigits >= self.precision {
370                // if the lhs already exceeds the desired precision, just align rhs
371                /* Before:
372                 * lhs: |==============|
373                 * rhs:      |==============|
374                 *              ediff  |<-->|
375                 *    precision  |<--->|
376                 *
377                 * After:
378                 * lhs: |==============|
379                 * rhs:      |=========|xxxx|
380                 */
381                let (rhs_signif, r) = split_digits_ref::<B>(&rhs.significand, ediff);
382                low = (rhs_sign * r, ediff);
383                (lhs.significand + rhs_sign * rhs_signif, lhs.exponent)
384            } else if self.is_limited() && ediff + ldigits > self.precision {
385                // if the shifted lhs exceeds the desired precision, align lhs and rhs to precision
386                /* Before:
387                 * lhs: |=========|
388                 * rhs:      |==============|
389                 *                |< ediff >|
390                 *      |< precision >|
391                 *
392                 * After:
393                 * lhs: |=========0000|
394                 * rhs:      |========|xxxxx|
395                 *        lshift  |<->|
396                 *            rshift  |<--->|
397                 */
398                let lshift = self.precision - ldigits;
399                let rshift = ediff - lshift;
400                let (rhs_signif, r) = split_digits_ref::<B>(&rhs.significand, rshift);
401                shl_digits_in_place::<B>(&mut lhs.significand, lshift);
402
403                low = (rhs_sign * r, rshift);
404                (lhs.significand + rhs_sign * rhs_signif, lhs.exponent - lshift as isize)
405            } else {
406                // otherwise directly shift lhs to required position
407                /* Before:
408                 * lhs: |==========|
409                 * rhs:       |==============|
410                 *                 |< ediff >|
411                 *      |<------ precision ------>|
412                 *
413                 * After:
414                 * lhs: |==========0000000000|
415                 * rhs:       |==============|
416                 */
417                shl_digits_in_place::<B>(&mut lhs.significand, ediff);
418                low = (IBig::ZERO, 0);
419                match rhs_sign {
420                    Positive => (lhs.significand + &rhs.significand, rhs.exponent),
421                    Negative => (lhs.significand - &rhs.significand, rhs.exponent),
422                }
423            };
424
425        self.repr_round_sum(significand, exponent, low, is_sub)
426    }
427
428    // lhs + rhs_sign * rhs, assuming lhs.exponent <= rhs.exponent
429    fn repr_add_small_large<const B: Word>(
430        &self,
431        lhs: Repr<B>,
432        rhs: &Repr<B>,
433        rhs_sign: Sign,
434    ) -> Rounded<Repr<B>> {
435        debug_assert!(lhs.exponent <= rhs.exponent);
436
437        // the following implementation should be exactly the same as `repr_add_large_small`
438        // other than lhs and rhs are swapped. See `repr_add_large_small` for full documentation
439        let is_sub = lhs.significand.sign() != rhs_sign * rhs.significand.sign();
440        let rnd_precision = self.precision + is_sub as usize;
441
442        let ediff = (rhs.exponent - lhs.exponent) as usize;
443        let rdigits = rhs.digits();
444        let ldigits_est = lhs.digits_ub();
445
446        // align the exponent
447        let low: (IBig, usize);
448        let (significand, exponent) =
449            if self.is_limited() && is_sub && ldigits_est + self.precision >= rdigits + ediff {
450                // Symmetric counterpart of the guard in `repr_add_large_small` (see there
451                // for the rationale); here the lower-exponent operand is `lhs`. Form the
452                // exact difference at full operand width and let the shared
453                // `repr_round_sum` round it once.
454                let rhs_signif = shl_digits::<B>(&rhs.significand, ediff);
455                low = (IBig::ZERO, 0);
456                (rhs_sign * rhs_signif + lhs.significand, lhs.exponent)
457            } else if self.is_limited()
458                && ldigits_est + 1 < ediff
459                && ldigits_est + 1 + rnd_precision < rdigits + ediff
460            {
461                // lhs is entirely below rhs's rounding window, so only its sign
462                // contributes; replace it with a unit sticky tail positioned at lhs's
463                // real magnitude (`ediff` digits below rhs's exponent). See
464                // `repr_add_large_small` for why the position must be `ediff` and not
465                // `precision - rdigits`.
466                low = (lhs.significand.signum(), ediff);
467                (rhs_sign * rhs.significand.clone(), rhs.exponent)
468            } else if self.is_limited() && rdigits >= self.precision {
469                // if the rhs already exceeds the desired precision, just align lhs
470                let (lhs_signif, r) = split_digits::<B>(lhs.significand, ediff);
471                low = (r, ediff);
472                match rhs_sign {
473                    Positive => (lhs_signif + &rhs.significand, rhs.exponent),
474                    Negative => (lhs_signif - &rhs.significand, rhs.exponent),
475                }
476            } else if self.is_limited() && ediff + rdigits > self.precision {
477                // if the shifted rhs exceeds the desired precision, align lhs and rhs to precision
478                let lshift = self.precision - rdigits;
479                let rshift = ediff - lshift;
480                let (lhs_signif, r) = split_digits::<B>(lhs.significand, rshift);
481                let rhs_signif = shl_digits::<B>(&rhs.significand, lshift);
482
483                low = (r, rshift);
484                (rhs_sign * rhs_signif + lhs_signif, rhs.exponent - lshift as isize)
485            } else {
486                // otherwise directly shift rhs to required position
487                let rhs_signif = shl_digits::<B>(&rhs.significand, ediff);
488                low = (IBig::ZERO, 0);
489                (rhs_sign * rhs_signif + lhs.significand, lhs.exponent)
490            };
491
492        self.repr_round_sum(significand, exponent, low, is_sub)
493    }
494
495    /// Add two floating point numbers under this context.
496    ///
497    /// # Examples
498    ///
499    /// ```
500    /// # use core::str::FromStr;
501    /// # use dashu_base::ParseError;
502    /// # use dashu_float::DBig;
503    /// use dashu_base::Approximation::*;
504    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
505    ///
506    /// let context = Context::<HalfAway>::new(2);
507    /// let a = DBig::from_str("1.234")?;
508    /// let b = DBig::from_str("6.789")?;
509    /// assert_eq!(context.add(&a.repr(), &b.repr()), Ok(Inexact(DBig::from_str("8.0")?, NoOp)));
510    /// # Ok::<(), ParseError>(())
511    /// ```
512    pub fn add<const B: Word>(&self, lhs: &Repr<B>, rhs: &Repr<B>) -> FpResult<FBig<R, B>> {
513        if lhs.is_infinite() || rhs.is_infinite() {
514            return Err(FpError::InfiniteInput);
515        }
516
517        let sum = if lhs.is_pos_zero() {
518            self.repr_round_ref(rhs)
519        } else if rhs.is_pos_zero() {
520            self.repr_round_ref(lhs)
521        } else {
522            match lhs.exponent.cmp(&rhs.exponent) {
523                Ordering::Equal => {
524                    let sig = &lhs.significand + &rhs.significand;
525                    self.repr_round(cancel_zero::<R, B>(sig, lhs.exponent))
526                }
527                Ordering::Greater => self.repr_add_large_small(lhs.clone(), rhs, Positive),
528                Ordering::Less => self.repr_add_small_large(lhs.clone(), rhs, Positive),
529            }
530        };
531        Ok(sum.map(|v| FBig::new(v, *self)))
532    }
533
534    /// Subtract two floating point numbers under this context.
535    ///
536    /// # Examples
537    ///
538    /// ```
539    /// # use core::str::FromStr;
540    /// # use dashu_base::ParseError;
541    /// # use dashu_float::DBig;
542    /// use dashu_base::Approximation::*;
543    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
544    ///
545    /// let context = Context::<HalfAway>::new(2);
546    /// let a = DBig::from_str("1.234")?;
547    /// let b = DBig::from_str("6.789")?;
548    /// assert_eq!(
549    ///     context.sub(&a.repr(), &b.repr()),
550    ///     Ok(Inexact(DBig::from_str("-5.6")?, SubOne))
551    /// );
552    /// # Ok::<(), ParseError>(())
553    /// ```
554    pub fn sub<const B: Word>(&self, lhs: &Repr<B>, rhs: &Repr<B>) -> FpResult<FBig<R, B>> {
555        if lhs.is_infinite() || rhs.is_infinite() {
556            return Err(FpError::InfiniteInput);
557        }
558
559        let sum = if lhs.is_pos_zero() {
560            // Round `-rhs` directly rather than negating *after* rounding. For the asymmetric
561            // modes (Up = toward +∞, Down = toward −∞), `round(-x) != -round(x)`: rounding
562            // `rhs` toward +∞ then negating rounds in the wrong direction, so `0 - rhs`
563            // would land one ULP off (e.g. truncated instead of rounded away from the result).
564            self.repr_round_ref(&Repr::new(-&rhs.significand, rhs.exponent))
565        } else if rhs.is_pos_zero() {
566            self.repr_round_ref(lhs)
567        } else {
568            match lhs.exponent.cmp(&rhs.exponent) {
569                Ordering::Equal => {
570                    let sig = &lhs.significand - &rhs.significand;
571                    self.repr_round(cancel_zero::<R, B>(sig, lhs.exponent))
572                }
573                Ordering::Greater => self.repr_add_large_small(lhs.clone(), rhs, Negative),
574                Ordering::Less => self.repr_add_small_large(lhs.clone(), rhs, Negative),
575            }
576        };
577        Ok(sum.map(|v| FBig::new(v, *self)))
578    }
579}
580
581#[cfg(test)]
582mod tests {
583    use super::*;
584    use crate::round::mode::{HalfAway, HalfEven};
585
586    // Build a normalized Repr from a small integer significand and an exponent.
587    fn r<const B: Word>(sig: i128, exp: isize) -> Repr<B> {
588        Repr::new(IBig::from(sig), exp)
589    }
590
591    // Severe cancellation must not collapse a genuinely small difference to 0.
592    // Pristine returned `0` for the first two rows: the trimmed alignment path
593    // keeps only a bounded low tail and its single re-expand can't recover the
594    // lost leading digits.
595    #[test]
596    fn sub_severe_cancellation_decimal() {
597        let ctx = Context::<HalfAway>::new(3);
598        // 1.00 - 0.99999999 = 1e-8 (exactly representable at precision 3)
599        assert_eq!(
600            ctx.sub(&r::<10>(100, -2), &r::<10>(99999999, -8))
601                .unwrap()
602                .value()
603                .repr(),
604            &r::<10>(1, -8)
605        );
606        // 1.00 - 0.99950001 = 4.9999e-4, rounds to 5.00e-4 (HalfAway)
607        assert_eq!(
608            ctx.sub(&r::<10>(100, -2), &r::<10>(99950001, -8))
609                .unwrap()
610                .value()
611                .repr(),
612            &r::<10>(500, -6)
613        );
614    }
615
616    #[test]
617    fn sub_severe_cancellation_binary() {
618        let ctx = Context::<HalfEven>::new(10);
619        // 2^20 - (2^20 - 1) = 1, with the operands 20 exponent positions apart
620        assert_eq!(
621            ctx.sub(&r::<2>(1, 20), &r::<2>((1i128 << 20) - 1, 0))
622                .unwrap()
623                .value()
624                .repr(),
625            &r::<2>(1, 0)
626        );
627        // same magnitude gap but the smaller-exponent operand is on the left
628        assert_eq!(
629            ctx.sub(&r::<2>((1i128 << 20) - 1, 0), &r::<2>(1, 20))
630                .unwrap()
631                .value()
632                .repr(),
633            &r::<2>(-1, 0)
634        );
635        // 2^30 - (2^30 - 1) = 1
636        assert_eq!(
637            ctx.sub(&r::<2>(1, 30), &r::<2>((1i128 << 30) - 1, 0))
638                .unwrap()
639                .value()
640                .repr(),
641            &r::<2>(1, 0)
642        );
643    }
644
645    // Effective subtraction reached through `Context::add` (opposite signs) must
646    // be fixed as well.
647    #[test]
648    fn add_effective_severe_cancellation() {
649        let ctx = Context::<HalfEven>::new(10);
650        // 2^20 + (-(2^20 - 1)) = 1
651        assert_eq!(
652            ctx.add(&r::<2>(1, 20), &r::<2>(-((1i128 << 20) - 1), 0))
653                .unwrap()
654                .value()
655                .repr(),
656            &r::<2>(1, 0)
657        );
658    }
659
660    // The public operator path (`a - b`) routes through the same kernel.
661    #[test]
662    fn sub_operator_severe_cancellation() {
663        let a = FBig::<HalfEven, 2>::from_parts(IBig::from(1), 20);
664        let b = FBig::<HalfEven, 2>::from_parts(IBig::from((1i128 << 20) - 1), 0);
665        assert_eq!((a - b).repr(), &r::<2>(1, 0));
666    }
667
668    // Mild subtractions — the smaller operand stays below the larger's precision
669    // window — must keep their existing behavior and not be diverted to the
670    // full-width path.
671    #[test]
672    fn sub_mild_unchanged() {
673        let ctx = Context::<HalfAway>::new(3);
674        // 101 - 0.2 = 100.8, kept as 1008 * 10^-1 (one guard digit, as before)
675        assert_eq!(
676            ctx.sub(&r::<10>(101, 0), &r::<10>(2, -1))
677                .unwrap()
678                .value()
679                .repr(),
680            &r::<10>(1008, -1)
681        );
682    }
683
684    // Regression for the branch-1 signum-proxy bug (SUM-BUG.md §2c): when the larger
685    // operand has fewer digits than the precision and a negligible operand is added,
686    // the sticky proxy must be positioned at the operand's *real* magnitude (`ediff`),
687    // not at `precision - ldigits`. The old positioning let the re-expand drag the
688    // sticky up to the LSB, where for base 2 + round-to-nearest it equals exactly half
689    // and injected a spurious ULP: `1 + 2^-100` at precision 10 gave `513*2^-9` (=
690    // 1.00195…) instead of `1`.
691    #[test]
692    fn add_negligible_short_operand_no_spurious_ulp() {
693        // base 2 + HalfAway: the exact tie case
694        let ctx = Context::<HalfAway>::new(10);
695        assert_eq!(
696            ctx.add(&r::<2>(1, 0), &r::<2>(1, -100))
697                .unwrap()
698                .value()
699                .repr(),
700            &r::<2>(1, 0)
701        );
702        assert_eq!(
703            ctx.sub(&r::<2>(1, 0), &r::<2>(1, -100))
704                .unwrap()
705                .value()
706                .repr(),
707            &r::<2>(1, 0)
708        );
709        // larger short operand (digits < precision), negligible addend
710        let ctx = Context::<HalfAway>::new(50);
711        assert_eq!(
712            ctx.add(&r::<2>(0x12345, 0), &r::<2>(1, -200))
713                .unwrap()
714                .value()
715                .repr(),
716            &r::<2>(0x12345, 0)
717        );
718        // base 10 was never affected (1 < ½·10), but check it stays correct
719        let ctx = Context::<HalfAway>::new(10);
720        assert_eq!(
721            ctx.add(&r::<10>(1, 0), &r::<10>(1, -100))
722                .unwrap()
723                .value()
724                .repr(),
725            &r::<10>(1, 0)
726        );
727    }
728}