Skip to content

libm-test: generator for varying specific bits#1182

Open
quaternic wants to merge 2 commits intorust-lang:mainfrom
quaternic:generate-bitpatterns
Open

libm-test: generator for varying specific bits#1182
quaternic wants to merge 2 commits intorust-lang:mainfrom
quaternic:generate-bitpatterns

Conversation

@quaternic
Copy link
Copy Markdown
Contributor

@quaternic quaternic commented Apr 5, 2026

for #1181

@quaternic
Copy link
Copy Markdown
Contributor Author

@tgross35
Given that I had this just collecting dust in an old git repo, it's surprisingly close to what you suggested. For example,

float_gen::<f32>(12, vec![0, !0, some_random_value])

would produce the 3 << 12 values with an exhaustive sweep on {sign, high 3 exponent bits, low 3 exponent bits, high 3 mantissa bits, low 2 mantissa bits} and the rest taken from each of the values in the Vec.

@quaternic quaternic force-pushed the generate-bitpatterns branch from 5e29c72 to 7faff1a Compare April 5, 2026 17:58
Comment on lines +3 to +29
/// An efficient equivalent to
/// `(0..=uN::MAX).filter(|x| x & !varying == 0).map(|x| x ^ preset)`
/// That is, "all integers that only differ from the preset in the varying bits"
pub struct BitConfig<I> {
/// A bitmask of bits to list exhaustively
varying: I,
/// Other bits are set according to preset.
preset: I,
}

impl<I: Int<Unsigned = I>> BitConfig<I> {
fn into_iter(self) -> impl Iterator<Item = I> + Clone {
assert!(
self.varying != I::MAX,
"to optimize the implementation, varying every bit is not supported"
);
let fixed = !self.varying;
let flip = self.preset ^ fixed;
let mut counter = fixed - I::ONE;

// `(counter + 1) & !fixed` is initially 0, and increases after each item returned
std::iter::from_fn(move || {
counter = counter.checked_add(I::ONE)? | fixed;
Some(counter ^ flip)
})
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be turned into a single function with a signature like the following:

fn bit_sequence<I, F>(varying: I, preset: F) -> impl Iterator<Item = I> + Clone
where
    I: Int,
    F: FnMut() -> I + Clone;

So the default could be created per-iteration with a rng?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC the only reason I had preset in there is that this way you can do the .map(|x| x ^ preset) "for free", but I now realize that a little bit of inlining should allow the optimizer to do that regardless. So it should be cleaner to just have

fn bitwise_subsets<I>(varying: I) -> impl Iterator<Item = I> + Clone

Comment on lines +41 to +55
let mut bit_priority: Vec<_> = (0..F::BITS).rev().collect();
// sign bit first, otherwise by least distance to any edge of a bitfield,
bit_priority[1..].sort_by_key(|&i| {
// avoid a fencepost error by mapping the bit indices to odd numbers,
// and compare them to the bitfield edges mapped to even integers
let i = 2 * i + 1;
i.min(i.abs_diff(F::SIG_BITS * 2))
.min(i.abs_diff((F::BITS - 1) * 2))
});

let varying = bit_priority[..bits_to_vary as usize]
.iter()
.map(|&i| F::Int::ONE << i)
.reduce(std::ops::BitOr::bitor)
.unwrap_or(F::Int::ZERO);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this portion be extracted to a separate function? The exact masks could then be asserted in tests.

It would be okay to drop the fillers portion or move it to tests since I think whatever wraps this for a test will need to do some tweaking around that.

Comment on lines +42 to +49
// sign bit first, otherwise by least distance to any edge of a bitfield,
bit_priority[1..].sort_by_key(|&i| {
// avoid a fencepost error by mapping the bit indices to odd numbers,
// and compare them to the bitfield edges mapped to even integers
let i = 2 * i + 1;
i.min(i.abs_diff(F::SIG_BITS * 2))
.min(i.abs_diff((F::BITS - 1) * 2))
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a quick run this seems to prefer putting the next bit into the upper end of the significand rather than the lower. I was thinking lower would be better since that's more likely to run into rounding issues.

Think it could also be worth asserting that bits_to_vary >= 5 to sanity check that we're at least toggling a bit at the top and bottom of the significand (n=4 for f16 is 0b1100011000000000, n=5 is 0b1100011000000001).

f16 exp:           0b0111110000000000
f16 sig:           0b0000001111111111
ternary f16 mask:  0b1111111110000111

f32 exp:           0b01111111100000000000000000000000
f32 sig:           0b00000000011111111111111111111111
binary f32 mask:   0b11111111111111000000000000001111
ternary f32 mask:  0b11110011111100000000000000000011

f64 exp:           0b0111111111110000000000000000000000000000000000000000000000000000
f64 sig:           0b0000000000001111111111111111111111111111111111111111111111111111
unary f64 mask:    0b1111111111111111111111110000000000000000000000000000111111111111
binary f64 mask:   0b1111110011111111000000000000000000000000000000000000000000001111
ternary f64 mask:  0b1111000001111110000000000000000000000000000000000000000000000011

f128 exp:          0b01111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
f128 sig:          0b00000000000000001111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
unary f128 mask:   0b11111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111111
binary f128 mask:  0b11111100000011111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111
ternary f128 mask: 0b11110000000001111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011

/// Biased generator for floats.
///
/// The returned iterator will produce `fillers.len() << bits_to_vary` items.
pub fn float_gen<F>(bits_to_vary: u32, fillers: Vec<F::Int>) -> impl Iterator<Item = F> + Clone
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For anything not yet used, remove the pub and mark it #[cfg_attr(not(test), expect(dead_code))] so we have a note of it.

@tgross35
Copy link
Copy Markdown
Contributor

tgross35 commented Apr 7, 2026

@tgross35 Given that I had this just collecting dust in an old git repo, it's surprisingly close to what you suggested. For example,

float_gen::<f32>(12, vec![0, !0, some_random_value])

would produce the 3 << 12 values with an exhaustive sweep on {sign, high 3 exponent bits, low 3 exponent bits, high 3 mantissa bits, low 2 mantissa bits} and the rest taken from each of the values in the Vec.

Wow no kidding, that's pretty much identical.

Are you interested in wiring up the tests in a followup? It would probably look like:

  • Come up with a similar generator for integers. Maybe split it in half and do the same thing? So bit order is something like 48732651. Or just reuse linear_ints for now.

  • Tie-in to the GeneratorKind enum in run_cfg

  • Update iteration_count and then use iteration_count(...).ilog2() - 1 (the 1 accounting for varying inner patterns) as the number of bits to vary. Or add a separate function, I'm not sure what is best here. (Unfortunately the runtime config is kind of messy... improvements welcome if you have ideas)

  • Add a trait and impls for creating the cases similar to SpacedInput. (I'm realizing these traits are kind of redundant; going to see if I can simplify things.)

  • Do one of the following:

    • Add a single get_test_cases function that runs once the default all zeros, once with all ones, and once with RNG output
    • Split them into three separate get_test_cases_* functions
    • Have one function for zeros+ones, and a separate one using an RNG.

    I think I would prefer the third because the rng version is useful to run on its own in a loop as a form of fuzzing that does slightly better than random.

  • In the multiprecision and compare_built_musl tests, add an invocation for each of these get_test_cases_* functions.

  • Can be a followup: figure out if we want to reduce or remove other generators. I think they're still useful to have but we can probably cut down the quickspace and random tests.

  • Followup: replace the current generator for extensive tests.

If so then it's of course appreciated, if not then it's just a todo list for me :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants