Skip to content

Commit 60a4828

Browse files
LorrensP-2158466RalfJung
authored andcommitted
make some powf and powi cases involving SNaN non-deterministic
1 parent 5ca574e commit 60a4828

File tree

2 files changed

+77
-38
lines changed

2 files changed

+77
-38
lines changed

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

Lines changed: 63 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use self::atomic::EvalContextExt as _;
1717
use self::helpers::{ToHost, ToSoft, check_intrinsic_arg_count};
1818
use self::simd::EvalContextExt as _;
1919
use crate::math::{IeeeExt, apply_random_float_error_ulp};
20+
use crate::operator::EvalContextExt as _;
2021
use crate::*;
2122

2223
impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
@@ -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();
@@ -312,7 +313,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
312313
let f1 = this.read_scalar(f1)?.to_f32()?;
313314
let f2 = this.read_scalar(f2)?.to_f32()?;
314315

315-
let res = fixed_float_value(intrinsic_name, &[f1, f2]).unwrap_or_else(|| {
316+
let res = fixed_float_value(this, intrinsic_name, &[f1, f2]).unwrap_or_else(|| {
316317
// Using host floats (but it's fine, this operation does not have guaranteed precision).
317318
let res = f1.to_host().powf(f2.to_host()).to_soft();
318319

@@ -330,7 +331,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
330331
let f1 = this.read_scalar(f1)?.to_f64()?;
331332
let f2 = this.read_scalar(f2)?.to_f64()?;
332333

333-
let res = fixed_float_value(intrinsic_name, &[f1, f2]).unwrap_or_else(|| {
334+
let res = fixed_float_value(this, intrinsic_name, &[f1, f2]).unwrap_or_else(|| {
334335
// Using host floats (but it's fine, this operation does not have guaranteed precision).
335336
let res = f1.to_host().powf(f2.to_host()).to_soft();
336337

@@ -349,7 +350,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
349350
let f = this.read_scalar(f)?.to_f32()?;
350351
let i = this.read_scalar(i)?.to_i32()?;
351352

352-
let res = fixed_powi_float_value(f, i).unwrap_or_else(|| {
353+
let res = fixed_powi_float_value(this, f, i).unwrap_or_else(|| {
353354
// Using host floats (but it's fine, this operation does not have guaranteed precision).
354355
let res = f.to_host().powi(i).to_soft();
355356

@@ -367,7 +368,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
367368
let f = this.read_scalar(f)?.to_f64()?;
368369
let i = this.read_scalar(i)?.to_i32()?;
369370

370-
let res = fixed_powi_float_value(f, i).unwrap_or_else(|| {
371+
let res = fixed_powi_float_value(this, f, i).unwrap_or_else(|| {
371372
// Using host floats (but it's fine, this operation does not have guaranteed precision).
372373
let res = f.to_host().powi(i).to_soft();
373374

@@ -496,49 +497,84 @@ fn apply_random_float_error_to_imm<'tcx>(
496497
/// - logf32, logf64, log2f32, log2f64, log10f32, log10f64
497498
/// - powf32, powf64
498499
///
500+
/// # Return
501+
///
499502
/// Returns `Some(output)` if the `intrinsic` results in a defined fixed `output` specified in the C standard
500503
/// (specifically, C23 annex F.10) when given `args` as arguments. Outputs that are unaffected by a relative error
501504
/// (such as INF and zero) are not handled here, they are assumed to be handled by the underlying
502505
/// implementation. Returns `None` if no specific value is guaranteed.
506+
///
507+
/// # Note
508+
///
509+
/// For `powf*` operations of the form:
510+
///
511+
/// - `(SNaN)^(±0)`
512+
/// - `1^(SNaN)`
513+
///
514+
/// The result is implementation-defined:
515+
/// - musl returns for both `1.0`
516+
/// - glibc returns for both `NaN`
517+
///
518+
/// This discrepancy exists because SNaN handling is not consistently defined across platforms,
519+
/// and the C standard leaves behavior for SNaNs unspecified.
520+
///
521+
/// Miri chooses to adhere to both implementations and returns either one of them non-deterministically.
503522
fn fixed_float_value<S: Semantics>(
523+
ecx: &mut MiriInterpCx<'_>,
504524
intrinsic_name: &str,
505525
args: &[IeeeFloat<S>],
506526
) -> Option<IeeeFloat<S>> {
507527
let one = IeeeFloat::<S>::one();
508-
match (intrinsic_name, args) {
528+
Some(match (intrinsic_name, args) {
509529
// cos(+- 0) = 1
510-
("cosf32" | "cosf64", [input]) if input.is_zero() => Some(one),
530+
("cosf32" | "cosf64", [input]) if input.is_zero() => one,
511531

512532
// e^0 = 1
513-
("expf32" | "expf64" | "exp2f32" | "exp2f64", [input]) if input.is_zero() => Some(one),
514-
515-
// 1^y = 1 for any y, even a NaN.
516-
("powf32" | "powf64", [base, _]) if *base == one => Some(one),
533+
("expf32" | "expf64" | "exp2f32" | "exp2f64", [input]) if input.is_zero() => one,
517534

518535
// (-1)^(±INF) = 1
519-
("powf32" | "powf64", [base, exp]) if *base == -one && exp.is_infinite() => Some(one),
536+
("powf32" | "powf64", [base, exp]) if *base == -one && exp.is_infinite() => one,
537+
538+
// 1^y = 1 for any y, even a NaN, *but* not a SNaN
539+
("powf32" | "powf64", [base, exp]) if *base == one => {
540+
let rng = ecx.machine.rng.get_mut();
541+
let return_nan = ecx.machine.float_nondet && rng.random() && exp.is_signaling();
542+
// Handle both the musl and glibc cases non-deterministically.
543+
if return_nan { ecx.generate_nan(args) } else { one }
544+
}
520545

521-
// FIXME(#4286): The C ecosystem is inconsistent with handling sNaN's, some return 1 others propogate
522-
// the NaN. We should return either 1 or the NaN non-deterministically here.
523-
// But for now, just handle them all the same.
524-
// x^(±0) = 1 for any x, even a NaN
525-
("powf32" | "powf64", [_, exp]) if exp.is_zero() => Some(one),
546+
// x^(±0) = 1 for any x, even a NaN, *but* not a SNaN
547+
("powf32" | "powf64", [base, exp]) if exp.is_zero() => {
548+
let rng = ecx.machine.rng.get_mut();
549+
let return_nan = ecx.machine.float_nondet && rng.random() && base.is_signaling();
550+
// Handle both the musl and glibc cases non-deterministically.
551+
if return_nan { ecx.generate_nan(args) } else { one }
552+
}
526553

527554
// There are a lot of cases for fixed outputs according to the C Standard, but these are mainly INF or zero
528555
// which are not affected by the applied error.
529-
_ => None,
530-
}
556+
_ => return None,
557+
})
531558
}
532559

533560
/// Returns `Some(output)` if `powi` (called `pown` in C) results in a fixed value specified in the C standard
534561
/// (specifically, C23 annex F.10.4.6) when doing `base^exp`. Otherwise, returns `None`.
535-
fn fixed_powi_float_value<S: Semantics>(base: IeeeFloat<S>, exp: i32) -> Option<IeeeFloat<S>> {
536-
match (base.category(), exp) {
537-
// x^0 = 1, if x is not a Signaling NaN
538-
// FIXME(#4286): The C ecosystem is inconsistent with handling sNaN's, some return 1 others propogate
539-
// the NaN. We should return either 1 or the NaN non-deterministically here.
540-
// But for now, just handle them all the same.
541-
(_, 0) => Some(IeeeFloat::<S>::one()),
562+
// TODO: I'm not sure what I should document here about pown(1, SNaN) since musl and glibc do the same and the C standard is explicit here.
563+
fn fixed_powi_float_value<S: Semantics>(
564+
ecx: &mut MiriInterpCx<'_>,
565+
base: IeeeFloat<S>,
566+
exp: i32,
567+
) -> Option<IeeeFloat<S>> {
568+
match exp {
569+
0 => {
570+
let one = IeeeFloat::<S>::one();
571+
let rng = ecx.machine.rng.get_mut();
572+
let return_nan = ecx.machine.float_nondet && rng.random() && base.is_signaling();
573+
Some(
574+
// Handle both the musl and glibc powf cases non-deterministically.
575+
if return_nan { ecx.generate_nan(&[base]) } else { one },
576+
)
577+
}
542578

543579
_ => None,
544580
}

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

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,17 +1066,6 @@ pub fn libm() {
10661066
assert_eq!((-1f32).powf(f32::NEG_INFINITY), 1.0);
10671067
assert_eq!((-1f64).powf(f64::NEG_INFINITY), 1.0);
10681068

1069-
// For pow (powf in rust) the C standard says:
1070-
// x^0 = 1 for all x even a sNaN
1071-
// FIXME(#4286): this does not match the behavior of all implementations.
1072-
assert_eq!(SNAN_F32.powf(0.0), 1.0);
1073-
assert_eq!(SNAN_F64.powf(0.0), 1.0);
1074-
1075-
// For pown (powi in rust) the C standard says:
1076-
// x^0 = 1 for all x even a sNaN
1077-
// FIXME(#4286): this does not match the behavior of all implementations.
1078-
assert_eq!(SNAN_F32.powi(0), 1.0);
1079-
assert_eq!(SNAN_F64.powi(0), 1.0);
10801069

10811070
assert_eq!(0f32.powi(10), 0.0);
10821071
assert_eq!(0f64.powi(100), 0.0);
@@ -1500,4 +1489,18 @@ fn test_non_determinism() {
15001489
test_operations_f32(12., 5.);
15011490
test_operations_f64(19., 11.);
15021491
test_operations_f128(25., 18.);
1492+
1493+
1494+
// x^(SNaN) = (1 | NaN)
1495+
ensure_nondet(|| f32::powf(SNAN_F32, 0.0));
1496+
ensure_nondet(|| f64::powf(SNAN_F64, 0.0));
1497+
1498+
// 1^(SNaN) = (1 | NaN)
1499+
ensure_nondet(|| f32::powf(1.0, SNAN_F32));
1500+
ensure_nondet(|| f64::powf(1.0, SNAN_F64));
1501+
1502+
// same as powf (keep it consistent):
1503+
// x^(SNaN) = (1 | NaN)
1504+
ensure_nondet(|| f32::powi(SNAN_F32, 0));
1505+
ensure_nondet(|| f64::powi(SNAN_F64, 0));
15031506
}

0 commit comments

Comments
 (0)