Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions compiler/rustc_span/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,7 @@ symbols! {
fields,
file,
final_associated_functions,
float_mul_add_relaxed,
float_to_int_unchecked,
floorf16,
floorf32,
Expand Down
12 changes: 12 additions & 0 deletions library/core/src/intrinsics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1475,6 +1475,9 @@ pub const fn fmaf128(a: f128, b: f128, c: f128) -> f128;
/// and add instructions. It is unspecified whether or not a fused operation
/// is selected, and that may depend on optimization level and context, for
/// example.
///
/// The stabilized version of this intrinsic is
/// [`f16::mul_add_relaxed`](../../std/primitive.f16.html#method.mul_add_relaxed)
#[inline]
#[rustc_intrinsic]
#[rustc_nounwind]
Expand All @@ -1491,6 +1494,9 @@ pub const fn fmuladdf16(a: f16, b: f16, c: f16) -> f16 {
/// and add instructions. It is unspecified whether or not a fused operation
/// is selected, and that may depend on optimization level and context, for
/// example.
///
/// The stabilized version of this intrinsic is
/// [`f32::mul_add_relaxed`](../../std/primitive.f32.html#method.mul_add_relaxed)
#[inline]
#[rustc_intrinsic]
#[rustc_nounwind]
Expand All @@ -1507,6 +1513,9 @@ pub const fn fmuladdf32(a: f32, b: f32, c: f32) -> f32 {
/// and add instructions. It is unspecified whether or not a fused operation
/// is selected, and that may depend on optimization level and context, for
/// example.
///
/// The stabilized version of this intrinsic is
/// [`f64::mul_add_relaxed`](../../std/primitive.f64.html#method.mul_add_relaxed)
#[inline]
#[rustc_intrinsic]
#[rustc_nounwind]
Expand All @@ -1523,6 +1532,9 @@ pub const fn fmuladdf64(a: f64, b: f64, c: f64) -> f64 {
/// and add instructions. It is unspecified whether or not a fused operation
/// is selected, and that may depend on optimization level and context, for
/// example.
///
/// The stabilized version of this intrinsic is
/// [`f128::mul_add_relaxed`](../../std/primitive.f128.html#method.mul_add_relaxed)
#[inline]
#[rustc_intrinsic]
#[rustc_nounwind]
Expand Down
32 changes: 32 additions & 0 deletions library/core/src/num/f128.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1836,6 +1836,38 @@ impl f128 {
intrinsics::fmaf128(self, a, b)
}

/// Computes `(self * a) + b` with nondeterministic rounding.
///
/// This is similar to [`mul_add`](Self::mul_add), but the intermediate
/// result may be rounded differently depending on the implementation.
/// The operation is either executed as a single fused multiply-add
/// instruction, or as separate multiply and add instructions.
///
/// The choice of which one is used is unspecified and non-deterministic:
/// it may vary by target, optimization level, and surrounding code, and
/// two evaluations of the same operation may even produce different
/// results.
///
/// # Examples
///
/// ```
/// #![feature(f128)]
/// #![feature(float_mul_add_relaxed)]
/// # #[cfg(any(miri, target_has_reliable_f128_math))] { // Miri uses softfloats, always works

@Mark-Simulacrum Mark-Simulacrum Jul 19, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, this seems suspicious.

I suspect this might be Claude-generated though, I don't see the same pattern repeated elsewhere (e.g., in f16 just below). @landsharkiest, is there some difference between f16/f128 miri behavior?

If this is needed I'll poke Miri folks to figure out how we can avoid it being needed, in the meantime I would drop miri from the cfg here. The right way to express this implication is for Miri to set target_has_reliable (if it's reliable) IMO.

View changes since the review

@RalfJung RalfJung Jul 19, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

target_has_reliable_f128 is always set in Miri, but target_has_reliable_f128_math is not since that also guards some operations Miri does not support.

Unfortunately target_has_reliable_f128_math guards a wide range of things, from stuff Miri can trivially support via softfloats (like mul_add) to things Miri cannot support because f128 isn't reliable on many of our targets (like pow, sin, log, etc).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, interesting. Maybe that suggests we need f128_simple_math? :)

Does the f16 situation differ for some reason (e.g., possible to add tables for literally all values?)?

If we think this is the right thing to do happy to r+ as-is though.

@RalfJung RalfJung Jul 19, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For f16 its similar. All the hosts that we build Miri on support it well enough that we can at least link pow, sin and friends without causing issues, and we set target_has_reliable_f16_math if the host has that flag. But actually for operations that we implement via softfloats we'd want target_has_reliable_f16_math to be unconditionally true, it's only the transcendental operations where hosts matter.

However it's probably not worth splitting up the cfg flag just for Miri.

Tons of existing tests have cfg(any(miri, target_has_reliable_f128_math)) so the new one here is consistent.

For f16 tons of tests actually use #[cfg(target_has_reliable_f16)] { which contradicts the intent of that flag as it was explained to me... 🤷 .

@RalfJung RalfJung Jul 19, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which contradicts the intent of that flag as it was explained to me

Also FWIW both f16 and f128 guard e.g. the tests for recip (1.0 / self) with the has_reliable flag, not the has_reliable_math flag, despite @tgross35 explaining to me that has_reliable is just for the ABI and bit-level operations (maybe including abs and copysign?) and has_reliable_math is for anything that needs actual math, rounding, or anything like that. For other operations (e.g. max, floor), f16 uses has_reliable but f128 uses has_reliable_math. So it's all quite inconsistent and messy I think.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

///
/// let result = 1.0f128.mul_add_relaxed(2.0, 3.0);
/// assert_eq!(result, 5.0);
/// # }
/// ```
#[inline]
#[rustc_allow_incoherent_impl]
#[doc(alias = "fmuladd")]
#[unstable(feature = "float_mul_add_relaxed", issue = "151770")]
#[must_use = "method returns a new number and does not mutate the original value"]
pub const fn mul_add_relaxed(self, a: f128, b: f128) -> f128 {
intrinsics::fmuladdf128(self, a, b)
}

/// Calculates Euclidean division, the matching method for `rem_euclid`.
///
/// This computes the integer `n` such that
Expand Down
32 changes: 32 additions & 0 deletions library/core/src/num/f16.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1822,6 +1822,38 @@ impl f16 {
intrinsics::fmaf16(self, a, b)
}

/// Computes `(self * a) + b` with nondeterministic rounding.
///
/// This is similar to [`mul_add`](Self::mul_add), but the intermediate
/// result may be rounded differently depending on the implementation.
/// The operation is either executed as a single fused multiply-add
/// instruction, or as separate multiply and add instructions.
///
/// The choice of which one is used is unspecified and non-deterministic:
/// it may vary by target, optimization level, and surrounding code, and
/// two evaluations of the same operation may even produce different
/// results.
///
/// # Examples
///
/// ```
/// #![feature(f16)]
/// #![feature(float_mul_add_relaxed)]
/// # #[cfg(target_has_reliable_f16)] {
///
/// let result = 1.0f16.mul_add_relaxed(2.0, 3.0);
/// assert_eq!(result, 5.0);
/// # }
/// ```
#[inline]
#[rustc_allow_incoherent_impl]
#[doc(alias = "fmuladd")]
#[unstable(feature = "float_mul_add_relaxed", issue = "151770")]
#[must_use = "method returns a new number and does not mutate the original value"]
pub const fn mul_add_relaxed(self, a: f16, b: f16) -> f16 {
intrinsics::fmuladdf16(self, a, b)
}

/// Calculates Euclidean division, the matching method for `rem_euclid`.
///
/// This computes the integer `n` such that
Expand Down
44 changes: 44 additions & 0 deletions library/core/src/num/f32.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1744,6 +1744,50 @@ impl f32 {
pub const fn algebraic_rem(self, rhs: f32) -> f32 {
intrinsics::frem_algebraic(self, rhs)
}

/// Computes `(self * a) + b` with nondeterministic rounding.
///
/// This is similar to [`mul_add`], but the intermediate result may be
/// rounded differently depending on the implementation. The operation is
/// either executed as a single fused multiply-add instruction, or as
/// separate multiply and add instructions.
///
/// The choice of which one is used is unspecified and non-deterministic:
/// it may vary by target, optimization level, and surrounding code, and
/// two evaluations of the same operation may even produce different
/// results.
///
/// # Precision
///
/// The result of this operation is not guaranteed: it is either the result
/// of [`mul_add`] (one rounding of the infinite-precision result) or of
/// `self * a + b` (two roundings, with an intermediate rounding of the
/// product).
///
/// # Examples
///
/// ```
/// #![feature(float_mul_add_relaxed)]
///
/// let result = 1.0f32.mul_add_relaxed(2.0, 3.0);
/// assert_eq!(result, 5.0);
///
/// // When the fused and unfused operations round differently, either
/// // result may be returned:
/// // - 5.2154064e-10 is the fused result (one rounding)
/// // - 9.313226e-10 is the unfused result (two roundings)
/// let r = 0.1_f32.mul_add_relaxed(0.1_f32, -0.01_f32);
/// assert!(r == 5.2154064e-10 || r == 9.313226e-10);
/// ```
///
/// [`mul_add`]: ../std/primitive.f32.html#method.mul_add
#[must_use = "method returns a new number and does not mutate the original value"]
#[doc(alias = "fmuladd")]
#[unstable(feature = "float_mul_add_relaxed", issue = "151770")]
#[inline]
pub const fn mul_add_relaxed(self, a: f32, b: f32) -> f32 {
intrinsics::fmuladdf32(self, a, b)
}
}

/// Experimental implementations of floating point functions in `core`.
Expand Down
44 changes: 44 additions & 0 deletions library/core/src/num/f64.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1724,6 +1724,50 @@ impl f64 {
pub const fn algebraic_rem(self, rhs: f64) -> f64 {
intrinsics::frem_algebraic(self, rhs)
}

/// Computes `(self * a) + b` with nondeterministic rounding.
///
/// This is similar to [`mul_add`], but the intermediate result may be
/// rounded differently depending on the implementation. The operation is
/// either executed as a single fused multiply-add instruction, or as
/// separate multiply and add instructions.
///
/// The choice of which one is used is unspecified and non-deterministic:
/// it may vary by target, optimization level, and surrounding code, and
/// two evaluations of the same operation may even produce different
/// results.
///
/// # Precision
///
/// The result of this operation is not guaranteed: it is either the result
/// of [`mul_add`] (one rounding of the infinite-precision result) or of
/// `self * a + b` (two roundings, with an intermediate rounding of the
/// product).
///
/// # Examples
///
/// ```
/// #![feature(float_mul_add_relaxed)]
///
/// let result = 1.0f64.mul_add_relaxed(2.0, 3.0);
/// assert_eq!(result, 5.0);
///
/// // When the fused and unfused operations round differently, either
/// // result may be returned:
/// // - 9.020562075079397e-19 is the fused result (one rounding)
/// // - 1.734723475976807e-18 is the unfused result (two roundings)
/// let r = 0.1_f64.mul_add_relaxed(0.1_f64, -0.01_f64);
/// assert!(r == 9.020562075079397e-19 || r == 1.734723475976807e-18);
/// ```
///
/// [`mul_add`]: ../std/primitive.f64.html#method.mul_add
#[must_use = "method returns a new number and does not mutate the original value"]
#[doc(alias = "fmuladd")]
#[unstable(feature = "float_mul_add_relaxed", issue = "151770")]
#[inline]
pub const fn mul_add_relaxed(self, a: f64, b: f64) -> f64 {
intrinsics::fmuladdf64(self, a, b)
}
}

#[unstable(feature = "core_float_math", issue = "137578")]
Expand Down
76 changes: 76 additions & 0 deletions tests/ui/intrinsics/float-mul-add-relaxed.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//@ run-pass
Comment thread
landsharkiest marked this conversation as resolved.
//@ compile-flags: -O

// Check that `mul_add_relaxed` returns either the fused result (one rounding)
// or the unfused result (two roundings), including with optimizations enabled
// where the operation may be const-folded or lowered to a fused instruction.

#![feature(float_mul_add_relaxed)]
#![feature(f16)]
#![feature(f128)]
#![feature(cfg_target_has_reliable_f16_f128)]
// `f16`/`f128` go unused on targets without reliable f16/f128 math, where the
// gated test functions below are compiled out.
#![allow(unused_features)]
// `target_has_reliable_*` are not "known" configs since they are unstable.
#![expect(unexpected_cfgs)]

use std::hint::black_box;

fn main() {
test_f32();
test_f64();
#[cfg(target_has_reliable_f16_math)]
test_f16();
#[cfg(target_has_reliable_f128_math)]
test_f128();
}

fn test_f32() {
// Exactly representable results are the same whether or not the
// operation is fused.
assert_eq!(black_box(2.0_f32).mul_add_relaxed(3.0, 4.0), 10.0);
assert_eq!(black_box(1.0_f32).mul_add_relaxed(1.0, 1.0), 2.0);

// `0.1 * 0.1` is inexact, so the fused (one rounding) and unfused (two
// roundings) results differ; either is allowed.
let r = black_box(0.1_f32).mul_add_relaxed(0.1, -0.01);
assert!(r == 5.2154064e-10 || r == 9.313226e-10);

// Edge cases behave like `a * b + c` regardless of fusion.
assert!(black_box(f32::NAN).mul_add_relaxed(1.0, 1.0).is_nan());
assert_eq!(black_box(f32::INFINITY).mul_add_relaxed(2.0, 1.0), f32::INFINITY);
assert!(black_box(0.0_f32).mul_add_relaxed(f32::INFINITY, 1.0).is_nan());
}

fn test_f64() {
assert_eq!(black_box(2.0_f64).mul_add_relaxed(3.0, 4.0), 10.0);
assert_eq!(black_box(1.0_f64).mul_add_relaxed(1.0, 1.0), 2.0);

let r = black_box(0.1_f64).mul_add_relaxed(0.1, -0.01);
assert!(r == 9.020562075079397e-19 || r == 1.734723475976807e-18);

@folkertdev folkertdev Jul 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can now make this

Suggested change
assert!(r == 9.020562075079397e-19 || r == 1.734723475976807e-18);
assert_matches!(r, 9.020562075079397e-19 | 1.734723475976807e-18);

and that should not just print that it failed, but also show the value that didn't match.

View changes since the review


assert!(black_box(f64::NAN).mul_add_relaxed(1.0, 1.0).is_nan());
assert_eq!(black_box(f64::INFINITY).mul_add_relaxed(2.0, 1.0), f64::INFINITY);
assert!(black_box(0.0_f64).mul_add_relaxed(f64::INFINITY, 1.0).is_nan());
}

#[cfg(target_has_reliable_f16_math)]
fn test_f16() {
assert_eq!(black_box(2.0_f16).mul_add_relaxed(3.0, 4.0), 10.0);
assert_eq!(black_box(1.0_f16).mul_add_relaxed(1.0, 1.0), 2.0);

assert!(black_box(f16::NAN).mul_add_relaxed(1.0, 1.0).is_nan());
assert_eq!(black_box(f16::INFINITY).mul_add_relaxed(2.0, 1.0), f16::INFINITY);
assert!(black_box(0.0_f16).mul_add_relaxed(f16::INFINITY, 1.0).is_nan());
}

#[cfg(target_has_reliable_f128_math)]
fn test_f128() {
assert_eq!(black_box(2.0_f128).mul_add_relaxed(3.0, 4.0), 10.0);
assert_eq!(black_box(1.0_f128).mul_add_relaxed(1.0, 1.0), 2.0);

assert!(black_box(f128::NAN).mul_add_relaxed(1.0, 1.0).is_nan());
assert_eq!(black_box(f128::INFINITY).mul_add_relaxed(2.0, 1.0), f128::INFINITY);
assert!(black_box(0.0_f128).mul_add_relaxed(f128::INFINITY, 1.0).is_nan());
}
Loading