bitflagset 0.0.4

Enum and positional typed bitsets with Set API
Documentation

bitflagset

Type-safe bitsets with Set-like ergonomics. Operations are direct primitive bit operations over words. Optional bitvec interop via the bitvec feature.

Design philosophy

Element-centric, not mask-centric. Every type exposes a HashSet/BTreeSet-style interface — contains(&V), insert(V) -> bool, remove(V) -> bool, is_subset, ... — while the underlying storage is a compact bit vector. The element type V is a generic parameter: an enum, a usize, or a named position constant. You work with domain values, never with raw bitmasks.

Deref-based method sharing. BitSet, BoxedBitSet, and array-backed BitSet<[T; N], V> all Deref to a shared unsized BitSlice<T, V> — a #[repr(transparent)] wrapper around [T]. Atomic types similarly share AtomicBitSlice<A, V>. Common methods are defined once on the slice type; owned types add storage-specific operations on top.

Word-level primitive operations. BitSlice operates on the raw [T] slice directly using count_ones(), bit masking, and word-level boolean operators.

Const-friendly primitives. For single-primitive BitSet<u64, V>, all query methods (len, is_empty, is_subset, ...) and constructors (from_bits, from_index, from_indices) are const fn. Build complex bitsets at compile time with zero runtime cost.

Full bit-operator support. Non-atomic owned types (BitSet, BoxedBitSet) implement BitOr, BitAnd, BitXor, Not, Sub and their Assign variants. Atomic types (AtomicBitSet, AtomicBoxedBitSet) expose equivalent set operations via named methods (union, difference, etc.) since atomics are inherently &self-based.

Performance

All numbers below are Criterion medians from cargo bench --bench compare, collected on 2026-09-30. Each competitor is two columns: its median, then × = their time / bitflagset on the printed times (one decimal, or an integer at 100 or above, e.g. 2.3×, 1202×). The bitflagset time and every competitor in that row come from the same Criterion group. A ratio in italics is a row where bitflagset's raw median is slower. — means that library has no equivalent in the bench. A column pair that is — on every row is dropped, and a row with no competitor value is dropped.
iter, union, intersection, and difference count iterator items. insert, remove, set, set_false, clear, union_with / or, intersect_with / and, and difference_with clone the destination in iter_batched_ref setup and time only the operation. Those in-place ops borrow the other set (union_from, intersect_from, difference_from), matching bit-set *_with and bit-vec or / and / difference. Fixed-size rows use BatchSize::SmallInput; the 65536-bit rows use BatchSize::LargeInput. len / count is len / count / count_ones. clear is clear / make_empty / fill(false). contains / get is contains / get. bitvec bitor / bitand / bitxor / not are by-value operators that build a new set. Atomic insert builds a fresh set and sets one bit (set_aliased on bitvec). On the 256/1024/65536 tables, (absent) uses a clear bit and (present) uses a set bit.

Non-atomic

BitSet<[u64; N]> / BoxedBitSet<u64> against bitvec BitArray / BitVec<u64>, bit_set::BitSet (default u32 blocks, with_capacity), and bit_vec::BitVec (default u32 blocks, from_elem). One group per size measures bitflagset and all three libraries.

256-bit ([u64; 4]):

Operation bitflagset bitvec × bit-set × bit-vec ×
insert 0.58 ns — — 1.67 ns 2.9× — —
remove (absent) 0.63 ns — — 1.28 ns 2.0× — —
remove (present) 0.83 ns — — 1.52 ns 1.8× — —
contains / get 0.82 ns 1.09 ns 1.3× 1.17 ns 1.4× 1.09 ns 1.3×
index 0.82 ns 1.09 ns 1.3× — — — —
len / count 2.09 ns 8.45 ns 4.0× 5.01 ns 2.4× 5.01 ns 2.4×
is_subset 0.82 ns — — 1.69 ns 2.1× — —
iter 2.09 ns 8.46 ns 4.0× 7.13 ns 3.4× — —
clear 0.57 ns — — 2.51 ns 4.4× 2.78 ns 4.9×
set 0.58 ns — — — — 2.28 ns 3.9×
set_false (absent) 0.63 ns — — — — 1.99 ns 3.2×
set_false (present) 0.83 ns — — — — 1.97 ns 2.4×
union 5.89 ns — — 13.99 ns 2.4× — —
intersection 5.18 ns — — 13.75 ns 2.7× — —
difference 6.07 ns — — 13.77 ns 2.3× — —
union_with / or 1.36 ns — — 9.05 ns 6.7× 8.22 ns 6.0×
intersect_with / and 1.60 ns — — 8.77 ns 5.5× 8.21 ns 5.1×
difference_with 1.41 ns — — 9.10 ns 6.5× 8.62 ns 6.1×
bitor 2.85 ns 51.58 ns 18.1× — — — —
bitand 2.85 ns 51.54 ns 18.1× — — — —
bitxor 2.85 ns 51.56 ns 18.1× — — — —
not 2.09 ns 2.69 ns 1.3× — — — —

1024-bit ([u64; 16]):

Operation bitflagset bitvec × bit-set × bit-vec ×
insert 1.24 ns — — 2.37 ns 1.9× — —
remove (absent) 1.25 ns — — 1.83 ns 1.5× — —
remove (present) 1.62 ns — — 2.28 ns 1.4× — —
contains / get 0.82 ns 1.09 ns 1.3× 1.16 ns 1.4× 1.09 ns 1.3×
index 0.82 ns 1.09 ns 1.3× — — — —
len / count 7.59 ns 32.47 ns 4.3× 18.25 ns 2.4× 18.25 ns 2.4×
is_subset 0.82 ns — — 1.70 ns 2.1× — —
iter 7.60 ns 32.44 ns 4.3× 18.49 ns 2.4× — —
clear 2.21 ns — — 2.58 ns 1.2× 2.55 ns 1.2×
set 1.23 ns — — — — 2.57 ns 2.1×
set_false (absent) 1.25 ns — — — — 2.58 ns 2.1×
set_false (present) 1.54 ns — — — — 2.57 ns 1.7×
union 9.43 ns — — 53.71 ns 5.7× — —
intersection 9.16 ns — — 53.69 ns 5.9× — —
difference 9.30 ns — — 53.72 ns 5.8× — —
union_with / or 3.15 ns — — 28.56 ns 9.1× 6.54 ns 2.1×
intersect_with / and 3.13 ns — — 28.57 ns 9.1× 6.51 ns 2.1×
difference_with 3.15 ns — — 28.61 ns 9.1× 6.97 ns 2.2×
bitor 9.81 ns 201.14 ns 20.5× — — — —
bitand 9.80 ns 201.09 ns 20.5× — — — —
bitxor 9.80 ns 201.18 ns 20.5× — — — —
not 7.43 ns 9.74 ns 1.3× — — — —

65536-bit (BoxedBitSet):

Operation bitflagset bitvec × bit-set × bit-vec ×
insert 1.34 ns — — 2.21 ns 1.6× — —
remove (absent) 1.64 ns — — 1.80 ns 1.1× — —
remove (present) 1.99 ns — — 2.11 ns 1.1× — —
contains / get 0.97 ns 1.12 ns 1.2× 1.16 ns 1.2× 1.09 ns 1.1×
index 0.97 ns 1.12 ns 1.2× — — — —
len / count 511.11 ns 1.69 µs 3.3× 1.13 µs 2.2× 1.14 µs 2.2×
is_subset 1.20 ns — — 1.66 ns 1.4× — —
iter 511.19 ns 1.69 µs 3.3× 1.13 µs 2.2× — —
clear 77.43 ns — — 77.54 ns 1.0× 77.64 ns 1.0×
set 1.32 ns — — — — 2.76 ns 2.1×
set_false (absent) 1.62 ns — — — — 2.54 ns 1.6×
set_false (present) 2.01 ns — — — — 2.48 ns 1.2×
union 570.26 ns — — 3.36 µs 5.9× — —
intersection 569.65 ns — — 3.36 µs 5.9× — —
difference 569.95 ns — — 3.36 µs 5.9× — —
union_with / or 178.61 ns — — 1.70 µs 9.5× 190.21 ns 1.1×
intersect_with / and 178.78 ns — — 1.70 µs 9.5× 190.46 ns 1.1×
difference_with 178.76 ns — — 1.70 µs 9.5× 216.20 ns 1.2×
bitor 411.51 ns 15.19 µs 36.9× — — — —
bitand 408.83 ns 14.63 µs 35.8× — — — —
bitxor 411.12 ns 15.20 µs 37.0× — — — —
not 243.82 ns 1.02 µs 4.2× — — — —

first / last are omitted: bit-set and bit-vec have neither. bit-set has no owned | / & / -; those producing operators are the bitvec bitor / bitand / bitxor / not rows. Iterator union / intersection / difference allocate nothing and count yielded indices. union_with / or, intersect_with / and, and difference_with are in place (union_from, intersect_from, difference_from versus bit-set *_with and bit-vec or / and / difference). bit-vec insert / remove shift the vector, so membership writes are set / set_false. bit-vec has no is_subset and no set-index iterator.

Slower rows:

None.

Atomic

AtomicBitSet<[AtomicU64; N]> / AtomicBoxedBitSet against bitvec BitArray<AtomicU64> / BitVec<AtomicU64>. bit-set and bit-vec have no atomic storage, so those columns are omitted.

256-bit ([AtomicU64; 4]):

Operation bitflagset bitvec ×
len 4.77 ns 8.45 ns 1.8×
is_empty 0.82 ns 8.45 ns 10.3×
contains 0.82 ns 0.82 ns 1.0×
index 0.82 ns 0.82 ns 1.0×
insert 2.13 ns 2.21 ns 1.0×
iter 4.77 ns 8.45 ns 1.8×

1024-bit ([AtomicU64; 16]):

Operation bitflagset bitvec ×
len 21.56 ns 32.16 ns 1.5×
is_empty 0.55 ns 32.18 ns 58.5×
contains 0.82 ns 0.82 ns 1.0×
index 0.82 ns 0.82 ns 1.0×
insert 2.59 ns 3.26 ns 1.3×
iter 21.56 ns 32.17 ns 1.5×

65536-bit (AtomicBoxedBitSet):

Operation bitflagset bitvec ×
len 1.38 µs 1.68 µs 1.2×
is_empty 0.82 ns 1.69 µs 2061×
contains 1.09 ns 1.10 ns 1.0×
index 1.09 ns 1.09 ns 1.0×
insert 107.62 ns 108.39 ns 1.0×
iter 1.35 µs 1.69 µs 1.3×

is_empty returns on the first non-zero word. Atomic contains loads the word with Relaxed ordering. Atomic insert, including the 65536-bit row, builds a fresh set inside the timed closure and sets one bit (set_aliased on BitVec<AtomicU64, Lsb0>).

Slower rows:

Row Ours Theirs Cause
256-bit index vs bitvec 0.82 ns 0.82 ns
1024-bit contains vs bitvec 0.82 ns 0.82 ns
1024-bit index vs bitvec 0.82 ns 0.82 ns

Types

Type Storage Thread-safe Deref target
BitSet<A, V> Single primitive No BitSlice<A, V>
BitSet<[T; N], V> Fixed-size array No BitSlice<T, V>
BoxedBitSet<T, V> Heap Box<[T]> No BitSlice<T, V>
AtomicBitSet<A, V> Atomic primitive Yes (direct methods)
AtomicBitSet<[A; N], V> Atomic array Yes AtomicBitSlice<A, V>
AtomicBoxedBitSet<A, V> Heap Box<[A]> Yes AtomicBitSlice<A, V>

Type aliases ArrayBitSet<A, V, N> and AtomicArrayBitSet<A, V, N> are provided for convenience.

Set interface

All types provide the standard collection methods:

use bitflagset::BitSet;

let mut a = BitSet::<u64, usize>::new();
a.insert(3);
a.insert(7);
a.insert(42);

assert!(a.contains(&7));
assert_eq!(a.len(), 3);

a.remove(7);
assert!(!a.contains(&7));

// Set algebra
let b = BitSet::<u64, usize>::from_element(3);
assert!(b.is_subset(&a));
assert!(a.is_superset(&b));

Bounds behavior

Breaking: out-of-range bit indices now panic in release builds.

An out-of-range bit index is a caller bug on every set type (primitive, array, boxed, atomic, and bitflagset! / atomic_bitflagset!).

  • contains / insert / remove / set / toggle / Index panic in debug and release with index {idx} out of range for capacity {cap}.
  • Const BitSet<primitive, usize>::contains, const from_index, and const bitflagset! contains / position from_element use the literal index out of range for capacity because formatting is not available in const fn.
  • set[i] (Index, output &bool) uses the element type as the key, so set[Color::Red] and set[5usize] both work. There is no IndexMut.
  • from_slice and from_bits_* still accept untrusted input: unknown bits are dropped or rejected as before. Enum from_element still debug-asserts.

Const construction

Primitive-backed BitSet supports const construction and queries:

use bitflagset::BitSet;

const FLAGS: BitSet<u64, usize> = BitSet::<u64, usize>::from_indices(&[3, 7, 42]);
const SINGLE: BitSet<u64, usize> = BitSet::<u64, usize>::from_index(5);
const RAW: BitSet<u64, usize> = BitSet::<u64, usize>::from_bits(0b1010);

// Const queries
const _: () = assert!(FLAGS.contains(&3));
const _: () = assert!(FLAGS.len() == 3);
const _: () = assert!(SINGLE.is_disjoint(&FLAGS));

Bit operators

use bitflagset::BitSet;

let a = [1u8, 4, 9].into_iter().collect::<BitSet<u64, u8>>();
let b = [4u8, 9, 15].into_iter().collect::<BitSet<u64, u8>>();

let union        = a | b;   // BitOr
let intersection = a & b;   // BitAnd
let sym_diff     = a ^ b;   // BitXor
let difference   = a - b;   // Sub
let complement   = !a;      // Not

All operators also have Assign variants (|=, &=, ^=, -=), including borrowed right-hand sides (|= &other). union_from, intersect_from, difference_from, and symmetric_difference_from are the named borrowed in-place forms.

Atomic bitsets

AtomicBitSet provides the same interface but all operations take &self. Mutations use AcqRel ordering; read-only methods (len, contains, iter, ...) use Relaxed.

use bitflagset::AtomicBitSet;
use std::sync::atomic::AtomicU64;

let flags = AtomicBitSet::<AtomicU64, usize>::new();
flags.insert(10);  // &self, not &mut self
assert!(flags.contains(&10));

Array-backed bitsets

For bit widths beyond a single primitive:

use bitflagset::BitSet;

let mut bs = BitSet::<[u64; 4], usize>::new(); // 256 bits
bs.insert(0);
bs.insert(127);
bs.insert(200);
let items: Vec<usize> = bs.iter().collect();
assert_eq!(items, vec![0, 127, 200]);

bitflagset! macro

Generates a named bitset type with element-centric Set API.

Enum form: wraps a #[repr(u8)] enum. Element type is the enum itself.

use bitflagset::{bitflag, bitflagset};

bitflag! {
    #[derive(Debug)]
    #[repr(u8)]
    enum Color {
        Red = 0,
        Green = 1,
        Blue = 2,
    }
}

bitflagset!(pub struct ColorSet(u8) : Color);

let mut set = ColorSet::from_slice(&[Color::Red, Color::Blue]);
assert!(set.contains(&Color::Red));
set.remove(Color::Green);

// Zero-cost conversion to/from BitSet
let bs: bitflagset::BitSet<u8, Color> = set.into();
let back: ColorSet = bs.into();

Position form: defines named constants as bit positions (not masks). Element type is u8.

use bitflagset::bitflagset;

bitflagset! {
    pub struct Perms(u8) {
        const READ = 0;
        const WRITE = 1;
        const EXEC = 2;
    }
}

let mut p = Perms::from_element(Perms::READ);
p.insert(Perms::WRITE);
assert!(p.contains(&Perms::READ));
assert_eq!(p.len(), 2);

// Composite constants via from_slice
impl Perms {
    const RW: Self = Self::from_slice(&[Self::READ, Self::WRITE]);
}
assert_eq!(p, Perms::RW);

// Same operators as enum form
let diff = Perms::all() - Perms::RW;
assert_eq!(diff, Perms::from_element(Perms::EXEC));

Both forms share the same Set-like interface: contains(&V), insert(V) -> bool, remove(V) -> bool, is_subset, is_superset, is_disjoint, plus full bit operators (|, &, ^, -, !).

Generated bitflagset! and atomic_bitflagset! structs are #[repr(transparent)] over their storage (u64, AtomicU64, ...), and AtomicBitSet<A, V> has the same layout as A. #[cfg] (and doc comments) on a position-form constant apply everywhere that constant is named, so a disabled flag is omitted from all(), names, and Debug. atomic_bitflagset!(struct AtomicX(AtomicU64) on X) links either an enum-form or a position-form X without repeating its constants.

bitvec interop (optional)

Enable the bitvec feature to get zero-cost conversions:

use bitflagset::BitSet;

let bs = BitSet::<u64, usize>::from_element(5);
let raw: &bitvec::slice::BitSlice<u64, bitvec::order::Lsb0> = bs.as_bitvec_slice();
assert!(raw[5]);

vs bitflags

bitflags bitflagset
Mental model mask-centric — constants are pre-shifted masks (0b01), API is mask-vs-mask (contains(Self)) element-centric — constants are positions (0, 1, 2), API is set-vs-element (contains(&V), insert(V) -> bool)
Atomic flags Not provided; use Mutex or manual AtomicU* atomic_bitflagset! provides lock-free insert/remove/toggle

License

MIT