Skip to content

Commit c2ff1b3

Browse files
committed
Completely overhaul fuzz testing
adds testing for almost every numerical intrinsic
1 parent 430c0b4 commit c2ff1b3

File tree

11 files changed

+937
-129
lines changed

11 files changed

+937
-129
lines changed

src/int/mod.rs

+11
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ pub trait Int:
7272
/// Prevents the need for excessive conversions between signed and unsigned
7373
fn logical_shr(self, other: u32) -> Self;
7474

75+
/// Absolute difference between two integers.
76+
fn abs_diff(self, other: Self) -> Self::UnsignedInt;
77+
7578
// copied from primitive integers, but put in a trait
7679
fn is_zero(self) -> bool;
7780
fn max_value() -> Self;
@@ -251,6 +254,10 @@ macro_rules! int_impl {
251254
me
252255
}
253256

257+
fn abs_diff(self, other: Self) -> Self {
258+
(self.wrapping_sub(other) as $ity).wrapping_abs() as $uty
259+
}
260+
254261
int_impl_common!($uty, $bits);
255262
}
256263

@@ -274,6 +281,10 @@ macro_rules! int_impl {
274281
me as $ity
275282
}
276283

284+
fn abs_diff(self, other: Self) -> $uty {
285+
self.wrapping_sub(other).wrapping_abs() as $uty
286+
}
287+
277288
int_impl_common!($ity, $bits);
278289
}
279290
};

testcrate/Cargo.toml

+1-1
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

+258
Original file line numberDiff line numberDiff line change
@@ -1 +1,259 @@
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.
12+
//!
13+
//! Some floating point tests are disabled for specific architectures, because they do not have
14+
//! correct rounding.
115
#![no_std]
16+
17+
use compiler_builtins::float::Float;
18+
use compiler_builtins::int::Int;
19+
20+
use rand_xoshiro::rand_core::{RngCore, SeedableRng};
21+
use rand_xoshiro::Xoshiro128StarStar;
22+
23+
/// Sets the number of fuzz iterations run for most tests. In practice, the vast majority of bugs
24+
/// are caught by the edge case testers. Most of the remaining bugs triggered by more complex
25+
/// sequences are caught well within 10_000 fuzz iterations. For classes of algorithms like division
26+
/// that are vulnerable to rare edge cases, we want 1_000_000 iterations to be more confident. In
27+
/// practical CI, however, we only want to run the more strenuous test once to catch algorithmic
28+
/// level bugs, and run the 10_000 iteration test on most targets. Target-dependent bugs are likely
29+
/// to involve miscompilation and misconfiguration that is likely to break algorithms in quickly
30+
/// caught ways. We choose to configure `N = 1_000_000` iterations for `x86_64` targets (and if
31+
/// debug assertions are disabled. Tests without `--release` would take too long) which are likely
32+
/// to have fast hardware, and run `N = 10_000` for all other targets.
33+
pub const N: u32 = if cfg!(target_arch = "x86_64") && !cfg!(debug_assertions) {
34+
1_000_000
35+
} else {
36+
10_000
37+
};
38+
39+
/// Random fuzzing step. When run several times, it results in excellent fuzzing entropy such as:
40+
/// 11110101010101011110111110011111
41+
/// 10110101010100001011101011001010
42+
/// 1000000000000000
43+
/// 10000000000000110111110000001010
44+
/// 1111011111111101010101111110101
45+
/// 101111111110100000000101000000
46+
/// 10000000110100000000100010101
47+
/// 1010101010101000
48+
fn fuzz_step<I: Int>(rng: &mut Xoshiro128StarStar, x: &mut I) {
49+
let ones = !I::ZERO;
50+
let bit_indexing_mask: u32 = I::BITS - 1;
51+
// It happens that all the RNG we need can come from one call. 7 bits are needed to index a
52+
// worst case 128 bit integer, and there are 4 indexes that need to be made plus 4 bits for
53+
// selecting operations
54+
let rng32 = rng.next_u32();
55+
56+
// Randomly OR, AND, and XOR randomly sized and shifted continuous strings of
57+
// ones with `lhs` and `rhs`.
58+
let r0 = bit_indexing_mask & rng32;
59+
let r1 = bit_indexing_mask & (rng32 >> 7);
60+
let mask = ones.wrapping_shl(r0).rotate_left(r1);
61+
match (rng32 >> 14) % 4 {
62+
0 => *x |= mask,
63+
1 => *x &= mask,
64+
// both 2 and 3 to make XORs as common as ORs and ANDs combined
65+
_ => *x ^= mask,
66+
}
67+
68+
// Alternating ones and zeros (e.x. 0b1010101010101010). This catches second-order
69+
// problems that might occur for algorithms with two modes of operation (potentially
70+
// there is some invariant that can be broken and maintained via alternating between modes,
71+
// breaking the algorithm when it reaches the end).
72+
let mut alt_ones = I::ONE;
73+
for _ in 0..(I::BITS / 2) {
74+
alt_ones <<= 2;
75+
alt_ones |= I::ONE;
76+
}
77+
let r0 = bit_indexing_mask & (rng32 >> 16);
78+
let r1 = bit_indexing_mask & (rng32 >> 23);
79+
let mask = alt_ones.wrapping_shl(r0).rotate_left(r1);
80+
match rng32 >> 30 {
81+
0 => *x |= mask,
82+
1 => *x &= mask,
83+
_ => *x ^= mask,
84+
}
85+
}
86+
87+
// We need macros like this, because `#![no_std]` prevents us from using iterators
88+
macro_rules! edge_cases {
89+
($I:ident, $case:ident, $inner:block) => {
90+
for i0 in 0..$I::FUZZ_NUM {
91+
let mask_lo = (!$I::UnsignedInt::ZERO).wrapping_shr($I::FUZZ_LENGTHS[i0] as u32);
92+
for i1 in i0..I::FUZZ_NUM {
93+
let mask_hi =
94+
(!$I::UnsignedInt::ZERO).wrapping_shl($I::FUZZ_LENGTHS[i1 - i0] as u32);
95+
let $case = I::from_unsigned(mask_lo & mask_hi);
96+
$inner
97+
}
98+
}
99+
};
100+
}
101+
102+
/// Feeds a series of fuzzing inputs to `f`. The fuzzer first uses an algorithm designed to find
103+
/// edge cases, followed by a more random fuzzer that runs `n` times.
104+
pub fn fuzz<I: Int, F: FnMut(I)>(n: u32, mut f: F) {
105+
// edge case tester. Calls `f` 210 times for u128.
106+
// zero gets skipped by the loop
107+
f(I::ZERO);
108+
edge_cases!(I, case, {
109+
f(case);
110+
});
111+
112+
// random fuzzer
113+
let mut rng = Xoshiro128StarStar::seed_from_u64(0);
114+
let mut x: I = Int::ZERO;
115+
for _ in 0..n {
116+
fuzz_step(&mut rng, &mut x);
117+
f(x)
118+
}
119+
}
120+
121+
/// The same as `fuzz`, except `f` has two inputs.
122+
pub fn fuzz_2<I: Int, F: Fn(I, I)>(n: u32, f: F) {
123+
// Check cases where the first and second inputs are zero. Both call `f` 210 times for `u128`.
124+
edge_cases!(I, case, {
125+
f(I::ZERO, case);
126+
});
127+
edge_cases!(I, case, {
128+
f(case, I::ZERO);
129+
});
130+
// Nested edge tester. Calls `f` 44100 times for `u128`.
131+
edge_cases!(I, case0, {
132+
edge_cases!(I, case1, {
133+
f(case0, case1);
134+
})
135+
});
136+
137+
// random fuzzer
138+
let mut rng = Xoshiro128StarStar::seed_from_u64(0);
139+
let mut x: I = I::ZERO;
140+
let mut y: I = I::ZERO;
141+
for _ in 0..n {
142+
fuzz_step(&mut rng, &mut x);
143+
fuzz_step(&mut rng, &mut y);
144+
f(x, y)
145+
}
146+
}
147+
148+
/// Tester for shift functions
149+
pub fn fuzz_shift<I: Int, F: Fn(I, u32)>(f: F) {
150+
// Shift functions are very simple and do not need anything other than shifting a small
151+
// set of random patterns for every fuzz length.
152+
let mut rng = Xoshiro128StarStar::seed_from_u64(0);
153+
let mut x: I = Int::ZERO;
154+
for i in 0..I::FUZZ_NUM {
155+
fuzz_step(&mut rng, &mut x);
156+
f(x, Int::ZERO);
157+
f(x, I::FUZZ_LENGTHS[i] as u32);
158+
}
159+
}
160+
161+
fn fuzz_float_step<F: Float>(rng: &mut Xoshiro128StarStar, f: &mut F) {
162+
let rng32 = rng.next_u32();
163+
// we need to fuzz the different parts of the float separately, because the masking on larger
164+
// significands will tend to set the exponent to all ones or all zeros frequently
165+
166+
// sign bit fuzzing
167+
let sign = (rng32 & 1) != 0;
168+
169+
// exponent fuzzing. Only 4 bits for the selector needed.
170+
let ones = (F::Int::ONE << F::EXPONENT_BITS) - F::Int::ONE;
171+
let r0 = (rng32 >> 1) % F::EXPONENT_BITS;
172+
let r1 = (rng32 >> 5) % F::EXPONENT_BITS;
173+
// custom rotate shift. Note that `F::Int` is unsigned, so we can shift right without smearing
174+
// the sign bit.
175+
let mask = if r1 == 0 {
176+
ones.wrapping_shr(r0)
177+
} else {
178+
let tmp = ones.wrapping_shr(r0);
179+
(tmp.wrapping_shl(r1) | tmp.wrapping_shr(F::EXPONENT_BITS - r1)) & ones
180+
};
181+
let mut exp = (f.repr() & F::EXPONENT_MASK) >> F::SIGNIFICAND_BITS;
182+
match (rng32 >> 9) % 4 {
183+
0 => exp |= mask,
184+
1 => exp &= mask,
185+
_ => exp ^= mask,
186+
}
187+
188+
// significand fuzzing
189+
let mut sig = f.repr() & F::SIGNIFICAND_MASK;
190+
fuzz_step(rng, &mut sig);
191+
sig &= F::SIGNIFICAND_MASK;
192+
193+
*f = F::from_parts(sign, exp, sig);
194+
}
195+
196+
macro_rules! float_edge_cases {
197+
($F:ident, $case:ident, $inner:block) => {
198+
for exponent in [
199+
F::Int::ZERO,
200+
F::Int::ONE,
201+
F::Int::ONE << (F::EXPONENT_BITS / 2),
202+
(F::Int::ONE << (F::EXPONENT_BITS - 1)) - F::Int::ONE,
203+
F::Int::ONE << (F::EXPONENT_BITS - 1),
204+
(F::Int::ONE << (F::EXPONENT_BITS - 1)) + F::Int::ONE,
205+
(F::Int::ONE << F::EXPONENT_BITS) - F::Int::ONE,
206+
]
207+
.iter()
208+
{
209+
for significand in [
210+
F::Int::ZERO,
211+
F::Int::ONE,
212+
F::Int::ONE << (F::SIGNIFICAND_BITS / 2),
213+
(F::Int::ONE << (F::SIGNIFICAND_BITS - 1)) - F::Int::ONE,
214+
F::Int::ONE << (F::SIGNIFICAND_BITS - 1),
215+
(F::Int::ONE << (F::SIGNIFICAND_BITS - 1)) + F::Int::ONE,
216+
(F::Int::ONE << F::SIGNIFICAND_BITS) - F::Int::ONE,
217+
]
218+
.iter()
219+
{
220+
for sign in [false, true].iter() {
221+
let $case = F::from_parts(*sign, *exponent, *significand);
222+
$inner
223+
}
224+
}
225+
}
226+
};
227+
}
228+
229+
pub fn fuzz_float<F: Float, E: Fn(F)>(n: u32, f: E) {
230+
float_edge_cases!(F, case, {
231+
f(case);
232+
});
233+
234+
// random fuzzer
235+
let mut rng = Xoshiro128StarStar::seed_from_u64(0);
236+
let mut x = F::ZERO;
237+
for _ in 0..n {
238+
fuzz_float_step(&mut rng, &mut x);
239+
f(x);
240+
}
241+
}
242+
243+
pub fn fuzz_float_2<F: Float, E: Fn(F, F)>(n: u32, f: E) {
244+
float_edge_cases!(F, case0, {
245+
float_edge_cases!(F, case1, {
246+
f(case0, case1);
247+
});
248+
});
249+
250+
// random fuzzer
251+
let mut rng = Xoshiro128StarStar::seed_from_u64(0);
252+
let mut x = F::ZERO;
253+
let mut y = F::ZERO;
254+
for _ in 0..n {
255+
fuzz_float_step(&mut rng, &mut x);
256+
fuzz_float_step(&mut rng, &mut y);
257+
f(x, y)
258+
}
259+
}

0 commit comments

Comments
 (0)