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}