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}