#250 Jul 9, 2026

250. lowest_one / highest_one — Set-Bit Positions as an Option, Not a Sentinel

trailing_zeros() on 0 returns the type’s width — a sentinel you must remember to special-case. Rust 1.97 adds lowest_one and highest_one, which return an Option and make “no bits set” impossible to forget.

The sentinel problem

This morning’s bite 249 covered bit_width from today’s Rust 1.97 release. The same release fixes another sharp edge: finding the position of a set bit.

The classic tools each handle “no bits set” badly:

1
2
3
4
5
6
7
8
let x = 0b0101_0100_u8;

// Position of the lowest set bit... usually.
assert_eq!(x.trailing_zeros(), 2);

// On zero it returns the type width — a magic
// number you must remember to check for:
assert_eq!(0_u8.trailing_zeros(), 8);

For the highest bit it’s worse: ilog2() panics on zero, and the leading_zeros arithmetic bakes the type width into your formula — the same trap bite 249 described.

An Option is honest

Stable since Rust 1.97 on all integer types:

1
2
3
4
5
6
7
8
let x = 0b0101_0100_u8;

assert_eq!(x.lowest_one(),  Some(2));
assert_eq!(x.highest_one(), Some(6));

// Zero has no set bits — and the type says so:
assert_eq!(0_u8.lowest_one(),  None);
assert_eq!(0_u8.highest_one(), None);

No sentinel, no panic. The compiler forces you to decide what “no bits” means for your code, instead of letting 8 masquerade as a bit position:

1
2
3
4
5
6
7
8
// e.g. first free slot in an allocation bitmap,
// where a full mask means "grow"
let free_mask = 0b0000_0000_u8;

match free_mask.lowest_one() {
    Some(slot) => println!("use slot {slot}"),
    None       => println!("all full, grow"),
}

The 1.97 bit family

Together with bite 249, the release completes a tidy family: bit_width (how many bits a value needs), isolate_lowest_one / isolate_highest_one (the bit as a mask), and lowest_one / highest_one (the bit as a position). They agree with each other, too:

1
2
3
4
let x = 0b0101_0100_u8;

assert_eq!(x.highest_one().map(|p| p + 1)
            .unwrap_or(0), x.bit_width());

If you still write x & x.wrapping_neg() or 31 - n.leading_zeros() from muscle memory, Rust 1.97 is your cue to stop.

← Previous 249. bit_width — How Many Bits Does This Number Need? (New in Rust 1.97) Next → 251. include_str! — Ship the File Inside the Binary, Skip the Runtime Read