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