Skip to content

Commit 6ce85d8

Browse files
implement powf(SNaN, 0) non-det
1 parent 100199c commit 6ce85d8

File tree

2 files changed

+71
-24
lines changed

2 files changed

+71
-24
lines changed

src/tools/miri/src/intrinsics/mod.rs

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mod simd;
66
use std::ops::Neg;
77

88
use rand::Rng;
9+
use rand::rngs::StdRng;
910
use rustc_abi::Size;
1011
use rustc_apfloat::ieee::{IeeeFloat, Semantics};
1112
use rustc_apfloat::{self, Float, Round};
@@ -191,7 +192,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
191192
let [f] = check_intrinsic_arg_count(args)?;
192193
let f = this.read_scalar(f)?.to_f32()?;
193194

194-
let res = fixed_float_value(intrinsic_name, &[f]).unwrap_or_else(||{
195+
let res = fixed_float_value(this, intrinsic_name, &[f]).unwrap_or_else(||{
195196
// Using host floats (but it's fine, these operations do not have
196197
// guaranteed precision).
197198
let host = f.to_host();
@@ -235,7 +236,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
235236
let [f] = check_intrinsic_arg_count(args)?;
236237
let f = this.read_scalar(f)?.to_f64()?;
237238

238-
let res = fixed_float_value(intrinsic_name, &[f]).unwrap_or_else(||{
239+
let res = fixed_float_value(this, intrinsic_name, &[f]).unwrap_or_else(||{
239240
// Using host floats (but it's fine, these operations do not have
240241
// guaranteed precision).
241242
let host = f.to_host();
@@ -324,7 +325,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
324325
let f1 = this.read_scalar(f1)?.to_f32()?;
325326
let f2 = this.read_scalar(f2)?.to_f32()?;
326327

327-
let res = fixed_float_value(intrinsic_name, &[f1, f2]).unwrap_or_else(|| {
328+
let res = fixed_float_value(this, intrinsic_name, &[f1, f2]).unwrap_or_else(|| {
328329
// Using host floats (but it's fine, this operation does not have guaranteed precision).
329330
let res = f1.to_host().powf(f2.to_host()).to_soft();
330331

@@ -342,7 +343,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
342343
let f1 = this.read_scalar(f1)?.to_f64()?;
343344
let f2 = this.read_scalar(f2)?.to_f64()?;
344345

345-
let res = fixed_float_value(intrinsic_name, &[f1, f2]).unwrap_or_else(|| {
346+
let res = fixed_float_value(this, intrinsic_name, &[f1, f2]).unwrap_or_else(|| {
346347
// Using host floats (but it's fine, this operation does not have guaranteed precision).
347348
let res = f1.to_host().powf(f2.to_host()).to_soft();
348349

@@ -501,45 +502,76 @@ fn apply_random_float_error_to_imm<'tcx>(
501502
interp_ok(ImmTy::from_scalar_int(res, val.layout))
502503
}
503504

505+
/// Returns either a SNaN or a QNaN, with a randomly generated payload.
506+
fn random_nan<S: Semantics>(rng: &mut StdRng) -> IeeeFloat<S> {
507+
if rng.random() {
508+
IeeeFloat::<S>::snan(Some(rng.random()))
509+
} else {
510+
IeeeFloat::<S>::qnan(Some(rng.random()))
511+
}
512+
}
513+
504514
/// For the intrinsics:
505515
/// - sinf32, sinf64
506516
/// - cosf32, cosf64
507517
/// - expf32, expf64, exp2f32, exp2f64
508518
/// - logf32, logf64, log2f32, log2f64, log10f32, log10f64
509519
/// - powf32, powf64
510520
///
521+
/// # Note
522+
///
523+
/// For `powf*` operations of the form:
524+
///
525+
/// - `x^(±0)` where `x` is a SNaN
526+
/// - `1^y` where `y` is SNaN
527+
///
528+
/// The result is implementation-defined:
529+
/// - musl returns for both `1.0`
530+
/// - glibc returns for both `NaN`
531+
///
532+
/// This discrepancy exists because SNaN handling is not consistently defined across platforms,
533+
/// and the C standard leaves behavior for SNaNs unspecified.
534+
///
535+
/// # Return
536+
///
511537
/// Returns `Some(output)` if the `intrinsic` results in a defined fixed `output` specified in the C standard
512538
/// (specifically, C23 annex F.10) when given `args` as arguments. Outputs that are unaffected by a relative error
513539
/// (such as INF and zero) are not handled here, they are assumed to be handled by the underlying
514540
/// implementation. Returns `None` if no specific value is guaranteed.
515-
fn fixed_float_value<S: Semantics>(
541+
fn fixed_float_value<'tcx, S: Semantics>(
542+
ecx: &mut MiriInterpCx<'tcx>,
516543
intrinsic_name: &str,
517544
args: &[IeeeFloat<S>],
518545
) -> Option<IeeeFloat<S>> {
519546
let one = IeeeFloat::<S>::one();
520-
match (intrinsic_name, args) {
547+
Some(match (intrinsic_name, args) {
521548
// cos(+- 0) = 1
522-
("cosf32" | "cosf64", [input]) if input.is_zero() => Some(one),
549+
("cosf32" | "cosf64", [input]) if input.is_zero() => one,
523550

524551
// e^0 = 1
525-
("expf32" | "expf64" | "exp2f32" | "exp2f64", [input]) if input.is_zero() => Some(one),
552+
("expf32" | "expf64" | "exp2f32" | "exp2f64", [input]) if input.is_zero() => one,
526553

527-
// 1^y = 1 for any y, even a NaN.
528-
("powf32" | "powf64", [base, _]) if *base == one => Some(one),
554+
// 1^y = 1 for any y, even a NaN
555+
("powf32" | "powf64", [base, _]) if *base == one => one,
529556

530557
// (-1)^(±INF) = 1
531-
("powf32" | "powf64", [base, exp]) if *base == -one && exp.is_infinite() => Some(one),
532-
533-
// FIXME(#4286): The C ecosystem is inconsistent with handling sNaN's, some return 1 others propogate
534-
// the NaN. We should return either 1 or the NaN non-deterministically here.
535-
// But for now, just handle them all the same.
536-
// x^(±0) = 1 for any x, even a NaN
537-
("powf32" | "powf64", [_, exp]) if exp.is_zero() => Some(one),
558+
("powf32" | "powf64", [base, exp]) if *base == -one && exp.is_infinite() => one,
559+
560+
// x^(±0) = 1 for any x, even a NaN, *but* not a SNaN
561+
("powf32" | "powf64", [base, exp]) if exp.is_zero() => {
562+
// Handle both the musl and glibc cases non-deterministically.
563+
if base.is_signaling() {
564+
let rng = ecx.machine.rng.get_mut();
565+
if rng.random() { one } else { random_nan(rng) }
566+
} else {
567+
one
568+
}
569+
}
538570

539571
// There are a lot of cases for fixed outputs according to the C Standard, but these are mainly INF or zero
540572
// which are not affected by the applied error.
541-
_ => None,
542-
}
573+
_ => return None,
574+
})
543575
}
544576

545577
/// Returns `Some(output)` if `powi` (called `pown` in C) results in a fixed value specified in the C standard

src/tools/miri/tests/pass/float.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,11 +1067,26 @@ pub fn libm() {
10671067
assert_eq!((-1f32).powf(f32::NEG_INFINITY), 1.0);
10681068
assert_eq!((-1f64).powf(f64::NEG_INFINITY), 1.0);
10691069

1070-
// For pow (powf in rust) the C standard says:
1071-
// x^0 = 1 for all x even a sNaN
1072-
// FIXME(#4286): this does not match the behavior of all implementations.
1073-
assert_eq!(SNAN_F32.powf(0.0), 1.0);
1074-
assert_eq!(SNAN_F64.powf(0.0), 1.0);
1070+
macro_rules! test_snan_pow {
1071+
($snan:expr, $pow_op:path) => {{
1072+
let mut nan_seen = false;
1073+
let mut one_seen = false;
1074+
1075+
for _ in 0..64 {
1076+
let res = $pow_op($snan, 0 as _);
1077+
nan_seen |= res.is_nan();
1078+
one_seen |= res == 1.0;
1079+
}
1080+
1081+
let op_as_str = stringify!($pow_op);
1082+
1083+
assert!(nan_seen && one_seen, "{}(SNaN, 0) should return both `NaN` or `1.0` randomly", op_as_str);
1084+
}};
1085+
}
1086+
1087+
test_snan_pow!(SNAN_F32, f32::powf);
1088+
test_snan_pow!(SNAN_F64, f64::powf);
1089+
10751090

10761091
// For pown (powi in rust) the C standard says:
10771092
// x^0 = 1 for all x even a sNaN

0 commit comments

Comments
 (0)