Skip to main content

dashu_float/
mul.rs

1use dashu_base::Sign::{self, *};
2use dashu_int::{IBig, UBig};
3
4use crate::{
5    add::cancel_zero,
6    error::{assert_finite_operands, FpError, FpResult},
7    fbig::FBig,
8    helper_macros,
9    repr::{Context, Repr, Word},
10    round::Round,
11};
12use core::cmp::Ordering;
13use core::ops::{Mul, MulAssign};
14
15/// Raw product of two finite reprs, attaching the XOR sign of the operands to a zero product
16/// (the significand product alone is `+0`, losing the sign).
17///
18/// Returns an error when the result exponent overflows or underflows `isize`.
19pub(crate) fn make_mul_repr<const B: Word>(
20    lhs: &Repr<B>,
21    rhs: &Repr<B>,
22) -> Result<Repr<B>, FpError> {
23    let significand = &lhs.significand * &rhs.significand;
24    if significand.is_zero() {
25        return Ok(if lhs.sign() != rhs.sign() {
26            Repr::neg_zero()
27        } else {
28            Repr::zero()
29        });
30    }
31    let sign = if lhs.sign() != rhs.sign() {
32        Negative
33    } else {
34        Positive
35    };
36    let exponent = lhs.exponent.checked_add(rhs.exponent).ok_or_else(|| {
37        debug_assert!(
38            lhs.exponent.is_positive() == rhs.exponent.is_positive(),
39            "checked_add overflow with mixed-sign exponents is impossible"
40        );
41        if lhs.exponent > 0 {
42            FpError::Overflow(sign)
43        } else {
44            FpError::Underflow(sign)
45        }
46    })?;
47    Repr::new(significand, exponent).check_finite_exponent()
48}
49
50macro_rules! unwrap_mul_repr {
51    ($result:expr, $context:expr) => {
52        match $result {
53            Ok(r) => r,
54            Err(FpError::Overflow(sign)) => {
55                return FBig::new(Repr::infinity_with_sign(sign), $context);
56            }
57            Err(FpError::Underflow(sign)) => {
58                return FBig::new(Repr::zero_with_sign(sign), $context);
59            }
60            Err(_) => unreachable!(),
61        }
62    };
63}
64
65impl<R: Round, const B: Word> Mul<&FBig<R, B>> for &FBig<R, B> {
66    type Output = FBig<R, B>;
67
68    #[inline]
69    fn mul(self, rhs: &FBig<R, B>) -> Self::Output {
70        assert_finite_operands(&self.repr, &rhs.repr);
71
72        let context = Context::max(self.context, rhs.context);
73        let repr = unwrap_mul_repr!(make_mul_repr(&self.repr, &rhs.repr), context);
74        FBig::new(context.repr_round(repr).value(), context)
75    }
76}
77
78impl<R: Round, const B: Word> Mul<&FBig<R, B>> for FBig<R, B> {
79    type Output = FBig<R, B>;
80
81    #[inline]
82    fn mul(self, rhs: &FBig<R, B>) -> Self::Output {
83        assert_finite_operands(&self.repr, &rhs.repr);
84
85        let context = Context::max(self.context, rhs.context);
86        let repr = unwrap_mul_repr!(make_mul_repr(&self.repr, &rhs.repr), context);
87        FBig::new(context.repr_round(repr).value(), context)
88    }
89}
90
91impl<R: Round, const B: Word> Mul<FBig<R, B>> for &FBig<R, B> {
92    type Output = FBig<R, B>;
93
94    #[inline]
95    fn mul(self, rhs: FBig<R, B>) -> Self::Output {
96        assert_finite_operands(&self.repr, &rhs.repr);
97
98        let context = Context::max(self.context, rhs.context);
99        let repr = unwrap_mul_repr!(make_mul_repr(&self.repr, &rhs.repr), context);
100        FBig::new(context.repr_round(repr).value(), context)
101    }
102}
103
104impl<R: Round, const B: Word> Mul<FBig<R, B>> for FBig<R, B> {
105    type Output = FBig<R, B>;
106
107    #[inline]
108    fn mul(self, rhs: FBig<R, B>) -> Self::Output {
109        assert_finite_operands(&self.repr, &rhs.repr);
110
111        let context = Context::max(self.context, rhs.context);
112        let repr = unwrap_mul_repr!(make_mul_repr(&self.repr, &rhs.repr), context);
113        FBig::new(context.repr_round(repr).value(), context)
114    }
115}
116
117helper_macros::impl_binop_assign_by_taking!(impl MulAssign<Self>, mul_assign, mul);
118
119macro_rules! impl_mul_primitive_with_fbig {
120    ($($t:ty)*) => {$(
121        helper_macros::impl_binop_with_primitive!(impl Mul<$t>, mul);
122        helper_macros::impl_binop_assign_with_primitive!(impl MulAssign<$t>, mul_assign);
123    )*};
124}
125impl_mul_primitive_with_fbig!(u8 u16 u32 u64 u128 usize UBig i8 i16 i32 i64 i128 isize IBig);
126
127impl<R: Round, const B: Word> FBig<R, B> {
128    /// Compute the square of this number (`self * self`)
129    ///
130    /// # Examples
131    ///
132    /// ```
133    /// # use core::str::FromStr;
134    /// # use dashu_base::ParseError;
135    /// # use dashu_float::DBig;
136    /// let a = DBig::from_str("-1.234")?;
137    /// assert_eq!(a.sqr(), DBig::from_str("1.523")?);
138    /// # Ok::<(), ParseError>(())
139    /// ```
140    #[inline]
141    pub fn sqr(&self) -> Self {
142        self.context.unwrap_fp(self.context.sqr(&self.repr))
143    }
144
145    /// Compute the cubic of this number (`self * self * self`)
146    ///
147    /// # Examples
148    ///
149    /// ```
150    /// # use core::str::FromStr;
151    /// # use dashu_base::ParseError;
152    /// # use dashu_float::DBig;
153    /// let a = DBig::from_str("-1.234")?;
154    /// assert_eq!(a.cubic(), DBig::from_str("-1.879")?);
155    /// # Ok::<(), ParseError>(())
156    /// ```
157    #[inline]
158    pub fn cubic(&self) -> Self {
159        self.context.unwrap_fp(self.context.cubic(&self.repr))
160    }
161
162    /// Fused multiply–add with a single rounding: `c + sign·(self * b)`.
163    ///
164    /// Unlike `(self * b) + c`, which rounds twice, `fma` rounds the exact
165    /// `self * b + c` once. `sign` scales the product: [`Sign::Positive`] gives
166    /// `self*b + c`, [`Sign::Negative`] gives `c − self*b`.
167    ///
168    /// # Examples
169    ///
170    /// ```
171    /// # use core::str::FromStr;
172    /// # use dashu_base::{ParseError, Sign};
173    /// # use dashu_float::DBig;
174    /// let a = DBig::from_str("1.5")?;
175    /// let b = DBig::from_str("2.0")?;
176    /// let c = DBig::from_str("0.1")?;
177    /// // 1.5*2.0 + 0.1 = 3.1
178    /// assert_eq!(a.fma(&b, &c, Sign::Positive), DBig::from_str("3.1")?);
179    /// // 0.1 − 1.5*2.0 = −2.9
180    /// assert_eq!(a.fma(&b, &c, Sign::Negative), DBig::from_str("-2.9")?);
181    /// # Ok::<(), ParseError>(())
182    /// ```
183    #[inline]
184    pub fn fma(&self, b: &Self, c: &Self, sign: Sign) -> Self {
185        let context = Context::max(self.context, Context::max(b.context, c.context));
186        context.unwrap_fp(context.fma(&self.repr, &b.repr, &c.repr, sign))
187    }
188}
189
190impl<R: Round> Context<R> {
191    /// Multiply two floating point numbers under this context.
192    ///
193    /// # Examples
194    ///
195    /// ```
196    /// # use core::str::FromStr;
197    /// # use dashu_base::ParseError;
198    /// # use dashu_float::DBig;
199    /// use dashu_base::Approximation::*;
200    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
201    ///
202    /// let context = Context::<HalfAway>::new(2);
203    /// let a = DBig::from_str("-1.234")?;
204    /// let b = DBig::from_str("6.789")?;
205    /// assert_eq!(
206    ///     context.mul(&a.repr(), &b.repr()),
207    ///     Ok(Inexact(DBig::from_str("-8.4")?, SubOne))
208    /// );
209    /// # Ok::<(), ParseError>(())
210    /// ```
211    pub fn mul<const B: Word>(&self, lhs: &Repr<B>, rhs: &Repr<B>) -> FpResult<FBig<R, B>> {
212        if lhs.is_infinite() || rhs.is_infinite() {
213            return Err(FpError::InfiniteInput);
214        }
215
216        // Exact product of the full operands, then round. (An earlier version shrank each operand
217        // to 2*precision before multiplying for speed, but that operand pre-rounding perturbs the
218        // product by the accumulated rounding error, so the result could land 1 ulp off the
219        // exact-product-rounded value near a rounding boundary. The exact product is always
220        // correctly rounded.)
221        let repr = make_mul_repr(lhs, rhs)?;
222        Ok(self.repr_round(repr).map(|v| FBig::new(v, *self)))
223    }
224
225    /// Calculate the square of the floating point number under this context.
226    ///
227    /// # Examples
228    ///
229    /// ```
230    /// # use core::str::FromStr;
231    /// # use dashu_base::ParseError;
232    /// # use dashu_float::DBig;
233    /// use dashu_base::Approximation::*;
234    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
235    ///
236    /// let context = Context::<HalfAway>::new(2);
237    /// let a = DBig::from_str("-1.234")?;
238    /// assert_eq!(context.sqr(&a.repr()), Ok(Inexact(DBig::from_str("1.5")?, NoOp)));
239    /// # Ok::<(), ParseError>(())
240    /// ```
241    pub fn sqr<const B: Word>(&self, f: &Repr<B>) -> FpResult<FBig<R, B>> {
242        if f.is_infinite() {
243            return Err(FpError::InfiniteInput);
244        }
245
246        // Exact square of the full significand, then round. (An earlier version shrank the operand
247        // to 2*precision before squaring, but that pre-rounding perturbs the square and could leave
248        // the result 1 ulp off the correctly-rounded value near a rounding boundary — same issue
249        // as `mul`. The dedicated `sqr` kernel is still used; it just gets the full significand.)
250        let exponent = f.exponent.checked_mul(2).ok_or({
251            // sqr always produces a non-negative result
252            if f.exponent > 0 {
253                FpError::Overflow(Positive)
254            } else {
255                FpError::Underflow(Positive)
256            }
257        })?;
258        let repr = Repr::new(f.significand.sqr().into(), exponent);
259        let repr = repr.check_finite_exponent()?;
260        Ok(self.repr_round(repr).map(|v| FBig::new(v, *self)))
261    }
262
263    /// Calculate the cubic of the floating point number under this context.
264    ///
265    /// # Examples
266    ///
267    /// ```
268    /// # use core::str::FromStr;
269    /// # use dashu_base::ParseError;
270    /// # use dashu_float::DBig;
271    /// use dashu_base::Approximation::*;
272    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
273    ///
274    /// let context = Context::<HalfAway>::new(2);
275    /// let a = DBig::from_str("-1.234")?;
276    /// assert_eq!(context.cubic(&a.repr()), Ok(Inexact(DBig::from_str("-1.9")?, SubOne)));
277    /// # Ok::<(), ParseError>(())
278    /// ```
279    pub fn cubic<const B: Word>(&self, f: &Repr<B>) -> FpResult<FBig<R, B>> {
280        if f.is_infinite() {
281            return Err(FpError::InfiniteInput);
282        }
283
284        // Exact cube of the full significand, then round. (An earlier version shrank the operand
285        // to 3*precision before cubing, but that pre-rounding perturbs the cube and could leave the
286        // result 1 ulp off the correctly-rounded value near a rounding boundary — same issue as
287        // `mul`. The dedicated `cubic` kernel is still used; it just gets the full significand.)
288        let repr = if f.significand.is_zero() {
289            // cubic(±0) = ±0 (odd power preserves sign)
290            if f.is_neg_zero() {
291                Repr::neg_zero()
292            } else {
293                Repr::zero()
294            }
295        } else {
296            let sign = f.sign();
297            let exponent = f.exponent.checked_mul(3).ok_or({
298                if f.exponent > 0 {
299                    FpError::Overflow(sign)
300                } else {
301                    FpError::Underflow(sign)
302                }
303            })?;
304            let repr = Repr::new(f.significand.cubic(), exponent);
305            repr.check_finite_exponent()?
306        };
307        Ok(self.repr_round(repr).map(|v| FBig::new(v, *self)))
308    }
309
310    /// Fused multiply–add under this context: `c + sign·(a·b)`, rounded once.
311    ///
312    /// The product `a·b` is formed exactly, then added to `c` with a single
313    /// rounding (reusing the aligned-then-round path of [`add`](Self::add), so the
314    /// severe-cancellation and sticky-tail handling is identical — including the
315    /// single guard digit an effective subtraction may leave in the result).
316    /// `sign` scales the product: [`Sign::Positive`] → `a·b + c`,
317    /// [`Sign::Negative`] → `c − a·b`.
318    ///
319    /// Returns [`FpError::InfiniteInput`] if any operand is infinite (matching
320    /// [`add`](Self::add)/[`mul`](Self::mul); dashu rejects infinite operands
321    /// outright, so the IEEE-754 `inf·0` / `inf−inf` indeterminate forms do not
322    /// arise). [`Overflow`](FpError::Overflow)/[`Underflow`](FpError::Underflow)
323    /// propagate from the product's exponent.
324    ///
325    /// # Examples
326    ///
327    /// ```
328    /// # use core::str::FromStr;
329    /// # use dashu_base::{Approximation::*, ParseError, Sign};
330    /// # use dashu_float::{Context, DBig, round::{mode::HalfAway, Rounding::*}};
331    /// let context = Context::<HalfAway>::new(2);
332    /// let a = DBig::from_str("1.5")?;
333    /// let b = DBig::from_str("2.0")?;
334    /// let c = DBig::from_str("0.1")?;
335    /// assert_eq!(
336    ///     context.fma(&a.repr(), &b.repr(), &c.repr(), Sign::Positive),
337    ///     Ok(Exact(DBig::from_str("3.1")?))
338    /// );
339    /// # Ok::<(), ParseError>(())
340    /// ```
341    pub fn fma<const B: Word>(
342        &self,
343        a: &Repr<B>,
344        b: &Repr<B>,
345        c: &Repr<B>,
346        sign: Sign,
347    ) -> FpResult<FBig<R, B>> {
348        if a.is_infinite() || b.is_infinite() || c.is_infinite() {
349            return Err(FpError::InfiniteInput);
350        }
351
352        // Exact product a·b. No operand shrinking (unlike Context::mul's 2p bound):
353        // a cancellation between the product and c can expose arbitrarily low
354        // product digits, so the full exact product is required for a correctly-
355        // rounded result.
356        let prod = make_mul_repr(a, b)?;
357
358        // Add c to sign·(a·b) with a single rounding. The product is exact, so the
359        // only rounding is in the add step — the same path as Context::add/sub.
360        let sum = if prod.significand.is_zero() {
361            // a·b == ±0: the signed zero product adds nothing to c.
362            self.repr_round_ref(c)
363        } else {
364            let signed_prod = if sign == Negative { prod.neg() } else { prod };
365            if c.significand.is_zero() {
366                // c == ±0: the result is sign·(a·b), rounded once.
367                self.repr_round(signed_prod)
368            } else {
369                match c.exponent.cmp(&signed_prod.exponent) {
370                    Ordering::Equal => self.repr_round(cancel_zero::<R, B>(
371                        &c.significand + signed_prod.significand,
372                        c.exponent,
373                    )),
374                    Ordering::Greater => {
375                        self.repr_add_large_small(c.clone(), &signed_prod, Positive)
376                    }
377                    Ordering::Less => self.repr_add_small_large(c.clone(), &signed_prod, Positive),
378                }
379            }
380        };
381        Ok(sum.map(|v| FBig::new(v, *self)))
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388    use crate::round::mode;
389    use dashu_int::IBig;
390
391    /// Reference: `c + sign·(a·b)` computed exactly at `4p+32` digits then rounded
392    /// down to `p`. A correctly-rounded `fma` must agree with this.
393    fn oracle<const B: Word, R: Round>(
394        a: &Repr<B>,
395        b: &Repr<B>,
396        c: &Repr<B>,
397        sign: Sign,
398        p: usize,
399    ) -> FBig<R, B> {
400        let hi = Context::<R>::new(p * 4 + 32);
401        let prod = hi.mul(a, b).unwrap().value();
402        let signed = if sign == Negative { -prod } else { prod };
403        let sum = hi.add(c, signed.repr()).unwrap().value();
404        sum.with_precision(p).value()
405    }
406
407    fn r<const B: Word>(sig: i128, exp: isize) -> Repr<B> {
408        Repr::new(IBig::from(sig), exp)
409    }
410
411    /// Force-round `v`'s significand to exactly `p` digits. (`with_precision` is a
412    /// no-op when the context precision already equals `p`; the guard digit an
413    /// effective subtraction leaves lives in the significand, beyond the context
414    /// precision, so it must be rounded away explicitly.)
415    fn round_sig<R: Round, const B: Word>(v: &FBig<R, B>, p: usize) -> FBig<R, B> {
416        let ctx = Context::<R>::new(p);
417        FBig::new(ctx.repr_round_ref(v.repr()).value(), ctx)
418    }
419
420    /// `fma` matches the high-precision oracle across fixed inputs, precisions,
421    /// both signs, base 10. (FMA reuses the add path, so on an effective
422    /// subtraction it may carry one guard digit — like `Context::sub` — so we
423    /// re-round to `p` before comparing to the exactly-`p` oracle.)
424    #[test]
425    fn test_fma_matches_oracle_decimal() {
426        // (a sig, a exp, b sig, b exp, c sig, c exp)
427        let cases: &[(i128, isize, i128, isize, i128, isize)] = &[
428            (15, -1, 20, -1, 10, -1),     // 1.5·2.0 + 0.1
429            (123, -2, 456, -2, 789, -2),  // 1.23·4.56 + 7.89
430            (101, -2, 99, -2, -9999, -4), // 1.01·0.99 − 0.9999 ≈ 0 (cancellation, a≠b)
431            (999, -2, 101, -1, -1, 2),    // 9.99·10.1 − 100 (mild cancel, diff exponents)
432        ];
433        for &(asg, ae, bsg, be, csg, ce) in cases {
434            for &p in &[2usize, 5, 20] {
435                let (a, b, c) = (r::<10>(asg, ae), r::<10>(bsg, be), r::<10>(csg, ce));
436                let ctx = Context::<mode::HalfAway>::new(p);
437                for sign in [Positive, Negative] {
438                    let got = ctx.fma(&a, &b, &c, sign).unwrap().value();
439                    let want = oracle::<10, mode::HalfAway>(&a, &b, &c, sign, p);
440                    assert_eq!(
441                        round_sig(&got, p),
442                        want,
443                        "fma mismatch p={p} sign={sign:?} a={asg}e{ae} b={bsg}e{be} c={csg}e{ce}"
444                    );
445                }
446            }
447        }
448    }
449
450    /// Base-2 spot check (HalfEven).
451    #[test]
452    fn test_fma_matches_oracle_binary() {
453        let (a, b, c) = (r::<2>(5, -2), r::<2>(3, -1), r::<2>(7, -3)); // 1.25, 1.5, 0.875
454        for &p in &[4usize, 10, 30] {
455            let ctx = Context::<mode::HalfEven>::new(p);
456            for sign in [Positive, Negative] {
457                let got = ctx.fma(&a, &b, &c, sign).unwrap().value();
458                let want = oracle::<2, mode::HalfEven>(&a, &b, &c, sign, p);
459                assert_eq!(round_sig(&got, p), want, "base-2 fma mismatch p={p} sign={sign:?}");
460            }
461        }
462    }
463
464    /// A zero product ⇒ result is `c`; a zero `c` ⇒ result is `a·b`.
465    #[test]
466    fn test_fma_zero_operands() {
467        let ctx = Context::<mode::HalfAway>::new(5);
468        let (z, a, c) = (r::<10>(0, 0), r::<10>(3, 0), r::<10>(7, 0));
469        // a·b == 0 (z·a): result is c.
470        assert_eq!(ctx.fma(&z, &a, &c, Positive).unwrap().value().repr(), &c);
471        // c == 0: result is a·b (3·3 = 9).
472        assert_eq!(ctx.fma(&a, &a, &z, Positive).unwrap().value().repr(), &r::<10>(9, 0));
473    }
474
475    /// Any infinite operand ⇒ `InfiniteInput`.
476    #[test]
477    fn test_fma_infinity_is_error() {
478        let ctx = Context::<mode::HalfAway>::new(5);
479        let (inf, a) = (Repr::<10>::infinity(), r::<10>(3, 0));
480        assert_eq!(ctx.fma(&inf, &a, &a, Positive), Err(FpError::InfiniteInput));
481        assert_eq!(ctx.fma(&a, &a, &inf, Positive), Err(FpError::InfiniteInput));
482    }
483
484    /// An exact-zero result is `-0` under roundTowardNegative (Down), exercising
485    /// the `cancel_zero` path (IEEE 754 §6.3).
486    #[test]
487    fn test_fma_exact_zero_is_neg_zero_under_down() {
488        let ctx = Context::<mode::Down>::new(5);
489        // 2·3 + (-6) = 0 exactly.
490        let (a, b, c) = (r::<10>(2, 0), r::<10>(3, 0), r::<10>(-6, 0));
491        let got = ctx.fma(&a, &b, &c, Positive).unwrap().value();
492        assert!(got.repr().is_neg_zero(), "expected -0, got {:?}", got.repr());
493    }
494}