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}