Skip to content

Commit 2dcc72d

Browse files
committed
Completely overhaul fuzz testing
1 parent 5516f39 commit 2dcc72d

File tree

12 files changed

+875
-130
lines changed

12 files changed

+875
-130
lines changed

src/float/mod.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ pub mod pow;
1313
pub mod sub;
1414

1515
/// Trait for some basic operations on floats
16-
pub(crate) trait Float:
16+
#[doc(hidden)]
17+
pub trait Float:
1718
Copy
1819
+ PartialEq
1920
+ PartialOrd
@@ -66,7 +67,6 @@ pub(crate) trait Float:
6667
/// Returns `self` transmuted to `Self::SignedInt`
6768
fn signed_repr(self) -> Self::SignedInt;
6869

69-
#[cfg(test)]
7070
/// Checks if two floats have the same bit representation. *Except* for NaNs! NaN can be
7171
/// represented in multiple different ways. This method returns `true` if two NaNs are
7272
/// compared.
@@ -106,7 +106,6 @@ macro_rules! float_impl {
106106
fn signed_repr(self) -> Self::SignedInt {
107107
unsafe { mem::transmute(self) }
108108
}
109-
#[cfg(test)]
110109
fn eq_repr(self, rhs: Self) -> bool {
111110
if self.is_nan() && rhs.is_nan() {
112111
true

testcrate/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ doctest = false
1111
[build-dependencies]
1212
rand = "0.7"
1313

14-
[dev-dependencies]
14+
[dependencies]
1515
# For fuzzing tests we want a deterministic seedable RNG. We also eliminate potential
1616
# problems with system RNGs on the variety of platforms this crate is tested on.
1717
# `xoshiro128**` is used for its quality, size, and speed at generating `u32` shift amounts.

testcrate/src/lib.rs

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,249 @@
1+
//! This crate is for integration testing and fuzz testing of functions in `compiler-builtins`. This
2+
//! includes publicly documented intrinsics and some internal alternative implementation functions
3+
//! such as `usize_leading_zeros_riscv` (which are tested because they are configured for
4+
//! architectures not tested by the CI).
5+
//!
6+
//! The general idea is to use a combination of edge case testing and randomized fuzz testing. The
7+
//! edge case testing is crucial for checking cases like where both inputs are equal or equal to
8+
//! special values such as `i128::MIN`, which is unlikely for the random fuzzer by itself to
9+
//! encounter. The randomized fuzz testing is specially designed to cover wide swaths of search
10+
//! space in as few iterations as possible. See `fuzz_values` in `testcrate/tests/misc.rs` for an
11+
//! example.
112
#![no_std]
13+
14+
use compiler_builtins::float::Float;
15+
use compiler_builtins::int::Int;
16+
17+
use rand_xoshiro::rand_core::{RngCore, SeedableRng};
18+
use rand_xoshiro::Xoshiro128StarStar;
19+
20+
/// Random fuzzing step. When run several times, it results in excellent fuzzing entropy such as:
21+
/// 11110101010101011110111110011111
22+
/// 10110101010100001011101011001010
23+
/// 1000000000000000
24+
/// 10000000000000110111110000001010
25+
/// 1111011111111101010101111110101
26+
/// 101111111110100000000101000000
27+
/// 10000000110100000000100010101
28+
/// 1010101010101000
29+
fn fuzz_step<I: Int>(rng: &mut Xoshiro128StarStar, x: &mut I) {
30+
let ones = !I::ZERO;
31+
let bit_indexing_mask: u32 = I::BITS - 1;
32+
// It happens that all the RNG we need can come from one call. 7 bits are needed to index a
33+
// worst case 128 bit integer, and there are 4 indexes that need to be made plus 4 bits for
34+
// selecting operations
35+
let rng32 = rng.next_u32();
36+
37+
// Randomly OR, AND, and XOR randomly sized and shifted continuous strings of
38+
// ones with `lhs` and `rhs`.
39+
let r0 = bit_indexing_mask & rng32;
40+
let r1 = bit_indexing_mask & (rng32 >> 7);
41+
let mask = ones.wrapping_shl(r0).rotate_left(r1);
42+
match (rng32 >> 14) % 4 {
43+
0 => *x |= mask,
44+
1 => *x &= mask,
45+
// both 2 and 3 to make XORs as common as ORs and ANDs combined
46+
_ => *x ^= mask,
47+
}
48+
49+
// Alternating ones and zeros (e.x. 0b1010101010101010). This catches second-order
50+
// problems that might occur for algorithms with two modes of operation (potentially
51+
// there is some invariant that can be broken and maintained via alternating between modes,
52+
// breaking the algorithm when it reaches the end).
53+
let mut alt_ones = I::ONE;
54+
for _ in 0..(I::BITS / 2) {
55+
alt_ones <<= 2;
56+
alt_ones |= I::ONE;
57+
}
58+
let r0 = bit_indexing_mask & (rng32 >> 16);
59+
let r1 = bit_indexing_mask & (rng32 >> 23);
60+
let mask = alt_ones.wrapping_shl(r0).rotate_left(r1);
61+
match rng32 >> 30 {
62+
0 => *x |= mask,
63+
1 => *x &= mask,
64+
_ => *x ^= mask,
65+
}
66+
}
67+
68+
macro_rules! edge_cases {
69+
($I:ident, $case:ident, $inner:block) => {
70+
for i0 in 0..$I::FUZZ_NUM {
71+
let mask_lo = (!$I::UnsignedInt::ZERO).wrapping_shr($I::FUZZ_LENGTHS[i0] as u32);
72+
for i1 in i0..I::FUZZ_NUM {
73+
let mask_hi =
74+
(!$I::UnsignedInt::ZERO).wrapping_shl($I::FUZZ_LENGTHS[i1 - i0] as u32);
75+
let $case = I::from_unsigned(mask_lo & mask_hi);
76+
$inner
77+
}
78+
}
79+
};
80+
}
81+
82+
/// Feeds a series of fuzzing inputs to `f`. The fuzzer first uses an algorithm designed to find
83+
/// edge cases, followed by a more random fuzzer that runs `n` times.
84+
pub fn fuzz<I: Int, F: FnMut(I)>(n: u32, mut f: F) {
85+
// edge case tester. Calls `f` 210 times for u128.
86+
// zero gets skipped by the loop
87+
f(I::ZERO);
88+
edge_cases!(I, case, {
89+
f(case);
90+
});
91+
92+
// random fuzzer
93+
let mut rng = Xoshiro128StarStar::seed_from_u64(0);
94+
let mut x: I = Int::ZERO;
95+
for _ in 0..n {
96+
fuzz_step(&mut rng, &mut x);
97+
f(x)
98+
}
99+
}
100+
101+
/// The same as `fuzz`, except `f` has two inputs.
102+
pub fn fuzz_2<I: Int, F: Fn(I, I)>(n: u32, f: F) {
103+
// Check cases where the first and second inputs are zero. Both call `f` 210 times for `u128`.
104+
edge_cases!(I, case, {
105+
f(I::ZERO, case);
106+
});
107+
edge_cases!(I, case, {
108+
f(case, I::ZERO);
109+
});
110+
// Nested edge tester. Calls `f` 44100 times for `u128`.
111+
edge_cases!(I, case0, {
112+
edge_cases!(I, case1, {
113+
f(case0, case1);
114+
})
115+
});
116+
117+
// random fuzzer
118+
let mut rng = Xoshiro128StarStar::seed_from_u64(0);
119+
let mut x: I = I::ZERO;
120+
let mut y: I = I::ZERO;
121+
for _ in 0..n {
122+
fuzz_step(&mut rng, &mut x);
123+
fuzz_step(&mut rng, &mut y);
124+
f(x, y)
125+
}
126+
}
127+
128+
/// Tester for shift functions
129+
pub fn fuzz_shift<I: Int, F: Fn(I, u32)>(f: F) {
130+
// Shift functions are very simple and do not need anything other than shifting a small
131+
// set of random patterns for every fuzz length.
132+
let mut rng = Xoshiro128StarStar::seed_from_u64(0);
133+
let mut x: I = Int::ZERO;
134+
for i in 0..I::FUZZ_NUM {
135+
fuzz_step(&mut rng, &mut x);
136+
f(x, I::FUZZ_LENGTHS[i] as u32);
137+
}
138+
}
139+
140+
fn fuzz_float_step<F: Float>(rng: &mut Xoshiro128StarStar, f: &mut F) {
141+
let rng32 = rng.next_u32();
142+
// we need to fuzz the different parts of the float separately, because the masking on larger
143+
// significands will tend to set the exponent to all ones or all zeros frequently
144+
145+
// sign bit fuzzing
146+
let sign = (rng32 & 1) != 0;
147+
148+
// exponent fuzzing. Only 4 bits for the selector needed.
149+
let ones = (F::Int::ONE << F::EXPONENT_BITS) - F::Int::ONE;
150+
let r0 = (rng32 >> 1) % F::EXPONENT_BITS;
151+
let r1 = (rng32 >> 5) % F::EXPONENT_BITS;
152+
// custom rotate shift. Note that `F::Int` is unsigned, so we can shift right without smearing
153+
// the sign bit.
154+
let mask = if r1 == 0 {
155+
ones.wrapping_shr(r0)
156+
} else {
157+
let tmp = ones.wrapping_shr(r0);
158+
(tmp.wrapping_shl(r1) | tmp.wrapping_shr(F::EXPONENT_BITS - r1)) & ones
159+
};
160+
let mut exp = (f.repr() & F::EXPONENT_MASK) >> F::SIGNIFICAND_BITS;
161+
match (rng32 >> 9) % 4 {
162+
0 => exp |= mask,
163+
1 => exp &= mask,
164+
_ => exp ^= mask,
165+
}
166+
167+
// significand fuzzing
168+
let r0 = (rng32 >> 11) % F::SIGNIFICAND_BITS;
169+
let r1 = (rng32 >> 18) % F::SIGNIFICAND_BITS;
170+
let mask = if r1 == 0 {
171+
F::SIGNIFICAND_MASK.wrapping_shr(r0)
172+
} else {
173+
let tmp = F::SIGNIFICAND_MASK.wrapping_shr(r0);
174+
(tmp.wrapping_shl(r1) | tmp.wrapping_shr(F::SIGNIFICAND_BITS - r1)) & F::SIGNIFICAND_MASK
175+
};
176+
let mut sig = f.repr() & F::SIGNIFICAND_MASK;
177+
match (rng32 >> 25) % 4 {
178+
0 => sig |= mask,
179+
1 => sig &= mask,
180+
_ => sig ^= mask,
181+
}
182+
183+
*f = F::from_parts(sign, exp, sig);
184+
}
185+
186+
macro_rules! float_edge_cases {
187+
($F:ident, $case:ident, $inner:block) => {
188+
for exponent in [
189+
F::Int::ZERO,
190+
F::Int::ONE,
191+
F::Int::ONE << (F::EXPONENT_BITS / 2),
192+
(F::Int::ONE << (F::EXPONENT_BITS - 1)) - F::Int::ONE,
193+
F::Int::ONE << (F::EXPONENT_BITS - 1),
194+
(F::Int::ONE << (F::EXPONENT_BITS - 1)) + F::Int::ONE,
195+
(F::Int::ONE << F::EXPONENT_BITS) - F::Int::ONE,
196+
]
197+
.iter()
198+
{
199+
for significand in [
200+
F::Int::ZERO,
201+
F::Int::ONE,
202+
F::Int::ONE << (F::SIGNIFICAND_BITS / 2),
203+
(F::Int::ONE << (F::SIGNIFICAND_BITS - 1)) - F::Int::ONE,
204+
F::Int::ONE << (F::SIGNIFICAND_BITS - 1),
205+
(F::Int::ONE << (F::SIGNIFICAND_BITS - 1)) + F::Int::ONE,
206+
(F::Int::ONE << F::SIGNIFICAND_BITS) - F::Int::ONE,
207+
]
208+
.iter()
209+
{
210+
for sign in [false, true].iter() {
211+
let $case = F::from_parts(*sign, *exponent, *significand);
212+
$inner
213+
}
214+
}
215+
}
216+
};
217+
}
218+
219+
pub fn fuzz_float<F: Float, E: Fn(F)>(n: u32, f: E) {
220+
float_edge_cases!(F, case, {
221+
f(case);
222+
});
223+
224+
// random fuzzer
225+
let mut rng = Xoshiro128StarStar::seed_from_u64(0);
226+
let mut x = F::ZERO;
227+
for _ in 0..n {
228+
fuzz_float_step(&mut rng, &mut x);
229+
f(x);
230+
}
231+
}
232+
233+
pub fn fuzz_float_2<F: Float, E: Fn(F, F)>(n: u32, f: E) {
234+
float_edge_cases!(F, case0, {
235+
float_edge_cases!(F, case1, {
236+
f(case0, case1);
237+
});
238+
});
239+
240+
// random fuzzer
241+
let mut rng = Xoshiro128StarStar::seed_from_u64(0);
242+
let mut x = F::ZERO;
243+
let mut y = F::ZERO;
244+
for _ in 0..n {
245+
fuzz_float_step(&mut rng, &mut x);
246+
fuzz_float_step(&mut rng, &mut y);
247+
f(x, y)
248+
}
249+
}

testcrate/tests/addsub.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
macro_rules! sum {
2+
($n:expr, $($i:ty, $fn_add:ident, $fn_sub:ident);*;) => {
3+
$(
4+
testcrate::fuzz_2($n, |x: $i, y: $i| {
5+
let add0 = x.wrapping_add(y);
6+
let sub0 = x.wrapping_sub(y);
7+
let add1: $i = $fn_add(x, y);
8+
let sub1: $i = $fn_sub(x, y);
9+
if add0 != add1 {
10+
panic!(
11+
"{}({}, {}): expected: {}, found: {}",
12+
stringify!($fn_add), x, y, add0, add1
13+
);
14+
}
15+
if sub0 != sub1 {
16+
panic!(
17+
"{}({}, {}): expected: {}, found: {}",
18+
stringify!($fn_sub), x, y, sub0, sub1
19+
);
20+
}
21+
});
22+
)*
23+
};
24+
}
25+
26+
macro_rules! overflowing_sum {
27+
($n:expr, $($i:ty, $fn_add:ident, $fn_sub:ident);*;) => {
28+
$(
29+
testcrate::fuzz_2($n, |x: $i, y: $i| {
30+
let add0 = x.overflowing_add(y);
31+
let sub0 = x.overflowing_sub(y);
32+
let add1: ($i, bool) = $fn_add(x, y);
33+
let sub1: ($i, bool) = $fn_sub(x, y);
34+
if add0.0 != add1.0 || add0.1 != add1.1 {
35+
panic!(
36+
"{}({}, {}): expected: {:?}, found: {:?}",
37+
stringify!($fn_add), x, y, add0, add1
38+
);
39+
}
40+
if sub0.0 != sub1.0 || sub0.1 != sub1.1 {
41+
panic!(
42+
"{}({}, {}): expected: {:?}, found: {:?}",
43+
stringify!($fn_sub), x, y, sub0, sub1
44+
);
45+
}
46+
});
47+
)*
48+
};
49+
}
50+
51+
#[test]
52+
fn addsub() {
53+
use compiler_builtins::int::addsub::{
54+
__rust_i128_add, __rust_i128_addo, __rust_i128_sub, __rust_i128_subo, __rust_u128_add,
55+
__rust_u128_addo, __rust_u128_sub, __rust_u128_subo,
56+
};
57+
58+
// Integer addition and subtraction is very simple, so 100 fuzzing passes should be plenty.
59+
sum!(100,
60+
u128, __rust_u128_add, __rust_u128_sub;
61+
i128, __rust_i128_add, __rust_i128_sub;
62+
);
63+
overflowing_sum!(100,
64+
u128, __rust_u128_addo, __rust_u128_subo;
65+
i128, __rust_i128_addo, __rust_i128_subo;
66+
);
67+
}
68+
69+
macro_rules! float_sum {
70+
($n:expr, $($f:ty, $fn_add:ident, $fn_sub:ident);*;) => {
71+
$(
72+
testcrate::fuzz_float_2($n, |x: $f, y: $f| {
73+
let add0 = x + y;
74+
let sub0 = x - y;
75+
let add1: $f = $fn_add(x, y);
76+
let sub1: $f = $fn_sub(x, y);
77+
if !Float::eq_repr(add0, add1) {
78+
panic!(
79+
"{}({}, {}): expected: {}, found: {}",
80+
stringify!($fn_add), x, y, add0, add1
81+
);
82+
}
83+
if !Float::eq_repr(sub0, sub1) {
84+
panic!(
85+
"{}({}, {}): expected: {}, found: {}",
86+
stringify!($fn_sub), x, y, sub0, sub1
87+
);
88+
}
89+
});
90+
)*
91+
};
92+
}
93+
94+
#[test]
95+
fn float_addsub() {
96+
use compiler_builtins::float::{
97+
add::{__adddf3, __addsf3},
98+
sub::{__subdf3, __subsf3},
99+
Float,
100+
};
101+
102+
float_sum!(10_000,
103+
f32, __addsf3, __subsf3;
104+
f64, __adddf3, __subdf3;
105+
);
106+
}

0 commit comments

Comments
 (0)