Skip to content

Commit 4fbe8b6

Browse files
committed
Rewrite the random test generator
Currently, all inputs are generated and then cached. This works reasonably well but it isn't very configurable or extensible (adding `f16` and `f128` is awkward). Replace this with a trait for generating random sequences of tuples. This also removes possible storage limitations of caching all inputs.
1 parent da610e5 commit 4fbe8b6

File tree

8 files changed

+148
-196
lines changed

8 files changed

+148
-196
lines changed

compiler-builtins/libm/crates/libm-test/benches/random.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ use std::hint::black_box;
22
use std::time::Duration;
33

44
use criterion::{Criterion, criterion_main};
5-
use libm_test::gen::{CachedInput, random};
6-
use libm_test::{CheckBasis, CheckCtx, GenerateInput, MathOp, TupleCall};
5+
use libm_test::gen::random;
6+
use libm_test::gen::random::RandomInput;
7+
use libm_test::{CheckBasis, CheckCtx, MathOp, TupleCall};
78

89
/// Benchmark with this many items to get a variety
910
const BENCH_ITER_ITEMS: usize = if cfg!(feature = "short-benchmarks") { 50 } else { 500 };
@@ -47,7 +48,7 @@ macro_rules! musl_rand_benches {
4748
fn bench_one<Op>(c: &mut Criterion, musl_extra: MuslExtra<Op::CFn>)
4849
where
4950
Op: MathOp,
50-
CachedInput: GenerateInput<Op::RustArgs>,
51+
Op::RustArgs: RandomInput,
5152
{
5253
let name = Op::NAME;
5354

Lines changed: 0 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
//! Different generators that can create random or systematic bit patterns.
22
3-
use crate::GenerateInput;
43
pub mod domain_logspace;
54
pub mod edge_cases;
65
pub mod random;
@@ -41,71 +40,3 @@ impl<I: Iterator> Iterator for KnownSize<I> {
4140
}
4241

4342
impl<I: Iterator> ExactSizeIterator for KnownSize<I> {}
44-
45-
/// Helper type to turn any reusable input into a generator.
46-
#[derive(Clone, Debug, Default)]
47-
pub struct CachedInput {
48-
pub inputs_f32: Vec<(f32, f32, f32)>,
49-
pub inputs_f64: Vec<(f64, f64, f64)>,
50-
pub inputs_i32: Vec<(i32, i32, i32)>,
51-
}
52-
53-
impl GenerateInput<(f32,)> for CachedInput {
54-
fn get_cases(&self) -> impl Iterator<Item = (f32,)> {
55-
self.inputs_f32.iter().map(|f| (f.0,))
56-
}
57-
}
58-
59-
impl GenerateInput<(f32, f32)> for CachedInput {
60-
fn get_cases(&self) -> impl Iterator<Item = (f32, f32)> {
61-
self.inputs_f32.iter().map(|f| (f.0, f.1))
62-
}
63-
}
64-
65-
impl GenerateInput<(i32, f32)> for CachedInput {
66-
fn get_cases(&self) -> impl Iterator<Item = (i32, f32)> {
67-
self.inputs_i32.iter().zip(self.inputs_f32.iter()).map(|(i, f)| (i.0, f.0))
68-
}
69-
}
70-
71-
impl GenerateInput<(f32, i32)> for CachedInput {
72-
fn get_cases(&self) -> impl Iterator<Item = (f32, i32)> {
73-
GenerateInput::<(i32, f32)>::get_cases(self).map(|(i, f)| (f, i))
74-
}
75-
}
76-
77-
impl GenerateInput<(f32, f32, f32)> for CachedInput {
78-
fn get_cases(&self) -> impl Iterator<Item = (f32, f32, f32)> {
79-
self.inputs_f32.iter().copied()
80-
}
81-
}
82-
83-
impl GenerateInput<(f64,)> for CachedInput {
84-
fn get_cases(&self) -> impl Iterator<Item = (f64,)> {
85-
self.inputs_f64.iter().map(|f| (f.0,))
86-
}
87-
}
88-
89-
impl GenerateInput<(f64, f64)> for CachedInput {
90-
fn get_cases(&self) -> impl Iterator<Item = (f64, f64)> {
91-
self.inputs_f64.iter().map(|f| (f.0, f.1))
92-
}
93-
}
94-
95-
impl GenerateInput<(i32, f64)> for CachedInput {
96-
fn get_cases(&self) -> impl Iterator<Item = (i32, f64)> {
97-
self.inputs_i32.iter().zip(self.inputs_f64.iter()).map(|(i, f)| (i.0, f.0))
98-
}
99-
}
100-
101-
impl GenerateInput<(f64, i32)> for CachedInput {
102-
fn get_cases(&self) -> impl Iterator<Item = (f64, i32)> {
103-
GenerateInput::<(i32, f64)>::get_cases(self).map(|(i, f)| (f, i))
104-
}
105-
}
106-
107-
impl GenerateInput<(f64, f64, f64)> for CachedInput {
108-
fn get_cases(&self) -> impl Iterator<Item = (f64, f64, f64)> {
109-
self.inputs_f64.iter().copied()
110-
}
111-
}
Lines changed: 102 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,118 @@
1-
//! A simple generator that produces deterministic random input, caching to use the same
2-
//! inputs for all functions.
3-
1+
use std::env;
2+
use std::ops::RangeInclusive;
43
use std::sync::LazyLock;
54

5+
use libm::support::Float;
6+
use rand::distributions::{Alphanumeric, Standard};
7+
use rand::prelude::Distribution;
68
use rand::{Rng, SeedableRng};
79
use rand_chacha::ChaCha8Rng;
810

9-
use super::CachedInput;
10-
use crate::{BaseName, CheckCtx, GenerateInput};
11-
12-
const SEED: [u8; 32] = *b"3.141592653589793238462643383279";
13-
14-
/// Number of tests to run.
15-
// FIXME(ntests): clean this up when possible
16-
const NTESTS: usize = {
17-
if cfg!(optimizations_enabled) {
18-
if crate::emulated()
19-
|| !cfg!(target_pointer_width = "64")
20-
|| cfg!(all(target_arch = "x86_64", target_vendor = "apple"))
21-
{
22-
// Tests are pretty slow on non-64-bit targets, x86 MacOS, and targets that run
23-
// in QEMU.
24-
100_000
25-
} else {
26-
5_000_000
27-
}
28-
} else {
29-
// Without optimizations just run a quick check
30-
800
31-
}
32-
};
33-
34-
/// Tested inputs.
35-
static TEST_CASES: LazyLock<CachedInput> = LazyLock::new(|| make_test_cases(NTESTS));
36-
37-
/// The first argument to `jn` and `jnf` is the number of iterations. Make this a reasonable
38-
/// value so tests don't run forever.
39-
static TEST_CASES_JN: LazyLock<CachedInput> = LazyLock::new(|| {
40-
// Start with regular test cases
41-
let mut cases = (*TEST_CASES).clone();
42-
43-
// These functions are extremely slow, limit them
44-
let ntests_jn = (NTESTS / 1000).max(80);
45-
cases.inputs_i32.truncate(ntests_jn);
46-
cases.inputs_f32.truncate(ntests_jn);
47-
cases.inputs_f64.truncate(ntests_jn);
48-
49-
// It is easy to overflow the stack with these in debug mode
50-
let max_iterations = if cfg!(optimizations_enabled) && cfg!(target_pointer_width = "64") {
51-
0xffff
52-
} else if cfg!(windows) {
53-
0x00ff
54-
} else {
55-
0x0fff
56-
};
11+
use super::KnownSize;
12+
use crate::run_cfg::{int_range, iteration_count};
13+
use crate::{CheckCtx, GeneratorKind};
5714

58-
let mut rng = ChaCha8Rng::from_seed(SEED);
15+
pub(crate) const SEED_ENV: &str = "LIBM_SEED";
5916

60-
for case in cases.inputs_i32.iter_mut() {
61-
case.0 = rng.gen_range(3..=max_iterations);
62-
}
17+
pub(crate) static SEED: LazyLock<[u8; 32]> = LazyLock::new(|| {
18+
let s = env::var(SEED_ENV).unwrap_or_else(|_| {
19+
let mut rng = rand::thread_rng();
20+
(0..32).map(|_| rng.sample(Alphanumeric) as char).collect()
21+
});
6322

64-
cases
23+
s.as_bytes().try_into().unwrap_or_else(|_| {
24+
panic!("Seed must be 32 characters, got `{s}`");
25+
})
6526
});
6627

67-
fn make_test_cases(ntests: usize) -> CachedInput {
68-
let mut rng = ChaCha8Rng::from_seed(SEED);
69-
70-
// make sure we include some basic cases
71-
let mut inputs_i32 = vec![(0, 0, 0), (1, 1, 1), (-1, -1, -1)];
72-
let mut inputs_f32 = vec![
73-
(0.0, 0.0, 0.0),
74-
(f32::EPSILON, f32::EPSILON, f32::EPSILON),
75-
(f32::INFINITY, f32::INFINITY, f32::INFINITY),
76-
(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY),
77-
(f32::MAX, f32::MAX, f32::MAX),
78-
(f32::MIN, f32::MIN, f32::MIN),
79-
(f32::MIN_POSITIVE, f32::MIN_POSITIVE, f32::MIN_POSITIVE),
80-
(f32::NAN, f32::NAN, f32::NAN),
81-
];
82-
let mut inputs_f64 = vec![
83-
(0.0, 0.0, 0.0),
84-
(f64::EPSILON, f64::EPSILON, f64::EPSILON),
85-
(f64::INFINITY, f64::INFINITY, f64::INFINITY),
86-
(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY),
87-
(f64::MAX, f64::MAX, f64::MAX),
88-
(f64::MIN, f64::MIN, f64::MIN),
89-
(f64::MIN_POSITIVE, f64::MIN_POSITIVE, f64::MIN_POSITIVE),
90-
(f64::NAN, f64::NAN, f64::NAN),
91-
];
92-
93-
inputs_i32.extend((0..(ntests - inputs_i32.len())).map(|_| rng.gen::<(i32, i32, i32)>()));
94-
95-
// Generate integers to get a full range of bitpatterns, then convert back to
96-
// floats.
97-
inputs_f32.extend((0..(ntests - inputs_f32.len())).map(|_| {
98-
let ints = rng.gen::<(u32, u32, u32)>();
99-
(f32::from_bits(ints.0), f32::from_bits(ints.1), f32::from_bits(ints.2))
100-
}));
101-
inputs_f64.extend((0..(ntests - inputs_f64.len())).map(|_| {
102-
let ints = rng.gen::<(u64, u64, u64)>();
103-
(f64::from_bits(ints.0), f64::from_bits(ints.1), f64::from_bits(ints.2))
104-
}));
105-
106-
CachedInput { inputs_f32, inputs_f64, inputs_i32 }
28+
/// Generate a sequence of random values of this type.
29+
pub trait RandomInput {
30+
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self>;
10731
}
10832

109-
/// Create a test case iterator.
110-
pub fn get_test_cases<RustArgs>(ctx: &CheckCtx) -> impl Iterator<Item = RustArgs>
33+
/// Generate a sequence of deterministically random floats.
34+
fn random_floats<F: Float>(count: u64) -> impl Iterator<Item = F>
11135
where
112-
CachedInput: GenerateInput<RustArgs>,
36+
Standard: Distribution<F::Int>,
11337
{
114-
let inputs = if ctx.base_name == BaseName::Jn || ctx.base_name == BaseName::Yn {
115-
&TEST_CASES_JN
116-
} else {
117-
&TEST_CASES
38+
let mut rng = ChaCha8Rng::from_seed(*SEED);
39+
40+
// Generate integers to get a full range of bitpatterns (including NaNs), then convert back
41+
// to the float type.
42+
(0..count).map(move |_| F::from_bits(rng.gen::<F::Int>()))
43+
}
44+
45+
/// Generate a sequence of deterministically random `i32`s within a specified range.
46+
fn random_ints(count: u64, range: RangeInclusive<i32>) -> impl Iterator<Item = i32> {
47+
let mut rng = ChaCha8Rng::from_seed(*SEED);
48+
(0..count).map(move |_| rng.gen_range::<i32, _>(range.clone()))
49+
}
50+
51+
macro_rules! impl_random_input {
52+
($fty:ty) => {
53+
impl RandomInput for ($fty,) {
54+
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
55+
let count = iteration_count(ctx, GeneratorKind::Random, 0);
56+
let iter = random_floats(count).map(|f: $fty| (f,));
57+
KnownSize::new(iter, count)
58+
}
59+
}
60+
61+
impl RandomInput for ($fty, $fty) {
62+
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
63+
let count0 = iteration_count(ctx, GeneratorKind::Random, 0);
64+
let count1 = iteration_count(ctx, GeneratorKind::Random, 1);
65+
let iter = random_floats(count0)
66+
.flat_map(move |f1: $fty| random_floats(count1).map(move |f2: $fty| (f1, f2)));
67+
KnownSize::new(iter, count0 * count1)
68+
}
69+
}
70+
71+
impl RandomInput for ($fty, $fty, $fty) {
72+
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
73+
let count0 = iteration_count(ctx, GeneratorKind::Random, 0);
74+
let count1 = iteration_count(ctx, GeneratorKind::Random, 1);
75+
let count2 = iteration_count(ctx, GeneratorKind::Random, 2);
76+
let iter = random_floats(count0).flat_map(move |f1: $fty| {
77+
random_floats(count1).flat_map(move |f2: $fty| {
78+
random_floats(count2).map(move |f3: $fty| (f1, f2, f3))
79+
})
80+
});
81+
KnownSize::new(iter, count0 * count1 * count2)
82+
}
83+
}
84+
85+
impl RandomInput for (i32, $fty) {
86+
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
87+
let count0 = iteration_count(ctx, GeneratorKind::Random, 0);
88+
let count1 = iteration_count(ctx, GeneratorKind::Random, 1);
89+
let range0 = int_range(ctx, 0);
90+
let iter = random_ints(count0, range0)
91+
.flat_map(move |f1: i32| random_floats(count1).map(move |f2: $fty| (f1, f2)));
92+
KnownSize::new(iter, count0 * count1)
93+
}
94+
}
95+
96+
impl RandomInput for ($fty, i32) {
97+
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
98+
let count0 = iteration_count(ctx, GeneratorKind::Random, 0);
99+
let count1 = iteration_count(ctx, GeneratorKind::Random, 1);
100+
let range1 = int_range(ctx, 1);
101+
let iter = random_floats(count0).flat_map(move |f1: $fty| {
102+
random_ints(count1, range1.clone()).map(move |f2: i32| (f1, f2))
103+
});
104+
KnownSize::new(iter, count0 * count1)
105+
}
106+
}
118107
};
119-
inputs.get_cases()
108+
}
109+
110+
impl_random_input!(f32);
111+
impl_random_input!(f64);
112+
113+
/// Create a test case iterator.
114+
pub fn get_test_cases<RustArgs: RandomInput>(
115+
ctx: &CheckCtx,
116+
) -> impl Iterator<Item = RustArgs> + use<'_, RustArgs> {
117+
RustArgs::get_cases(ctx)
120118
}

compiler-builtins/libm/crates/libm-test/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub use num::{FloatExt, logspace};
2626
pub use op::{BaseName, FloatTy, Identifier, MathOp, OpCFn, OpFTy, OpRustFn, OpRustRet, Ty};
2727
pub use precision::{MaybeOverride, SpecialCase, default_ulp};
2828
pub use run_cfg::{CheckBasis, CheckCtx, EXTENSIVE_ENV, GeneratorKind};
29-
pub use test_traits::{CheckOutput, GenerateInput, Hex, TupleCall};
29+
pub use test_traits::{CheckOutput, Hex, TupleCall};
3030

3131
/// Result type for tests is usually from `anyhow`. Most times there is no success value to
3232
/// propagate.

compiler-builtins/libm/crates/libm-test/src/run_cfg.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
//! Configuration for how tests get run.
22
3-
use std::env;
3+
use std::ops::RangeInclusive;
44
use std::sync::LazyLock;
5+
use std::{env, str};
56

7+
use crate::gen::random::{SEED, SEED_ENV};
68
use crate::{BaseName, FloatTy, Identifier, test_log};
79

810
/// The environment variable indicating which extensive tests should be run.
@@ -188,9 +190,16 @@ pub fn iteration_count(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -
188190
};
189191
let total = ntests.pow(t_env.input_count.try_into().unwrap());
190192

193+
let seed_msg = match gen_kind {
194+
GeneratorKind::Domain => String::new(),
195+
GeneratorKind::Random => {
196+
format!(" using `{SEED_ENV}={}`", str::from_utf8(SEED.as_slice()).unwrap())
197+
}
198+
};
199+
191200
test_log(&format!(
192201
"{gen_kind:?} {basis:?} {fn_ident} arg {arg}/{args}: {ntests} iterations \
193-
({total} total)",
202+
({total} total){seed_msg}",
194203
basis = ctx.basis,
195204
fn_ident = ctx.fn_ident,
196205
arg = argnum + 1,
@@ -200,6 +209,25 @@ pub fn iteration_count(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -
200209
ntests
201210
}
202211

212+
/// Some tests require that an integer be kept within reasonable limits; generate that here.
213+
pub fn int_range(ctx: &CheckCtx, argnum: usize) -> RangeInclusive<i32> {
214+
let t_env = TestEnv::from_env(ctx);
215+
216+
if !matches!(ctx.base_name, BaseName::Jn | BaseName::Yn) {
217+
return i32::MIN..=i32::MAX;
218+
}
219+
220+
assert_eq!(argnum, 0, "For `jn`/`yn`, only the first argument takes an integer");
221+
222+
// The integer argument to `jn` is an iteration count. Limit this to ensure tests can be
223+
// completed in a reasonable amount of time.
224+
if t_env.slow_platform || !cfg!(optimizations_enabled) {
225+
(-0xf)..=0xff
226+
} else {
227+
(-0xff)..=0xffff
228+
}
229+
}
230+
203231
/// For domain tests, limit how many asymptotes or specified check points we test.
204232
pub fn check_point_count(ctx: &CheckCtx) -> usize {
205233
let t_env = TestEnv::from_env(ctx);

0 commit comments

Comments
 (0)