Skip to content
This repository was archived by the owner on Apr 28, 2025. It is now read-only.

Commit addbb18

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 ae8bf8c commit addbb18

File tree

8 files changed

+148
-196
lines changed

8 files changed

+148
-196
lines changed

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

crates/libm-test/src/gen.rs

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-
}

crates/libm-test/src/gen/random.rs

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
}

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.

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)