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}