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