Repository: tigerbeetle/tigerbeetle
Pull request #2833: stdx: vendor bitset
Author: login: matklad
Labels: name: fuzz vopr
Created: 2025-03-20T15:57:02Z | Merged: 2025-03-24T19:59:46Z

Vendor another type from stdx to std! The standard motivation applies:
we want to insualte us from std API chunrn, reduce the risk of
"meaningless" (to us) changes subtly breaking the sematics we rely on,
and align APIs with TIGER_STYLE.

The extra specific motivation in this case is that stdlib type provides
both `initFull` and `initEmpty` symmetrically, but `initFull` is a
binary size footgun --- you must include the entire bitset into the text
section of Elf binary! The `initEmpty` works by `memset`, rather than
`memcpy`, at runtime. And, although symmetrict, std only provides
`findFirstSet`, not `findFirstUnset`, which forces us to use initFull at
times.

This change reducess the release binary size on Linux by 38KiB.

Additionally, there are a couple of "nice to have" changes:

- std version comptime dispatches between a small integer-backed bitset,
  and a large array-backed one. We instead _assert_ that the bitset is
  reasonably small (if it isn't, you should use DynamicBitset, to avoid
  specializing on length).
- I decided to avoid odd-sized integers (eg, `u3`) as a backign integer
  for the bitset, to make sure we rely on simpler compiler machinery.
- It feels like we sometimes used a "flipped" bitset just to gain
  `findFirstSet`, now we rely on the natural empty semantics more often.
- I removed some extra API surface, like bidirectional iteration over
  set/unset. We _might_ end up adding it back later, but my hunch is
  that a minimal API would be enough.


Diff:
diff --git a/src/iops.zig b/src/iops.zig
index 9e70293123..aafdf3c1a5 100644
--- a/src/iops.zig
+++ b/src/iops.zig
@@ -1,53 +1,56 @@
 const std = @import("std");
+const stdx = @import("./stdx.zig");
 const assert = std.debug.assert;
 
 /// Take a u8 to limit to 256 items max (2^8 = 256)
 pub fn IOPSType(comptime T: type, comptime size: u8) type {
-    const Map = std.StaticBitSet(size);
+    const Map = stdx.BitSetType(size);
+
     return struct {
         const IOPS = @This();
 
         items: [size]T = undefined,
-        /// 1 bits are free items.
-        free: Map = Map.initFull(),
+        busy: Map = .{},
 
         pub fn acquire(self: *IOPS) ?*T {
-            const i = self.free.findFirstSet() orelse return null;
-            self.free.unset(i);
+            const i = self.busy.first_unset() orelse return null;
+            self.busy.set(i);
             return &self.items[i];
         }
 
         pub fn release(self: *IOPS, item: *T) void {
             item.* = undefined;
             const i = self.index(item);
-            assert(!self.free.isSet(i));
-            self.free.set(i);
+            assert(self.busy.is_set(i));
+            self.busy.unset(i);
         }
 
         pub fn index(self: *IOPS, item: *T) usize {
-            const i = @divExact((@intFromPtr(item) - @intFromPtr(&self.items)), @sizeOf(T));
+            const i = @divExact(
+                (@intFromPtr(item) - @intFromPtr(&self.items)),
+                @sizeOf(T),
+            );
             assert(i < size);
             return i;
         }
 
         /// Returns the count of IOPs available.
         pub fn available(self: *const IOPS) usize {
-            return self.free.count();
+            return self.busy.capacity() - self.busy.count();
         }
 
-        pub fn total(self: *const IOPS) usize {
-            _ = self;
+        pub inline fn total(_: *const IOPS) usize {
             return size;
         }
 
         /// Returns the count of IOPs in use.
         pub fn executing(self: *const IOPS) usize {
-            return self.total() - self.available();
+            return self.busy.count();
         }
 
         pub const Iterator = struct {
             iops: *IOPS,
-            bitset_iterator: Map.Iterator(.{ .kind = .unset }),
+            bitset_iterator: Map.Iterator,
 
             pub fn next(iterator: *@This()) ?*T {
                 const i = iterator.bitset_iterator.next() orelse return null;
@@ -57,7 +60,7 @@ pub fn IOPSType(comptime T: type, comptime size: u8) type {
 
         pub const IteratorConst = struct {
             iops: *const IOPS,
-            bitset_iterator: Map.Iterator(.{ .kind = .unset }),
+            bitset_iterator: Map.Iterator,
 
             pub fn next(iterator: *@This()) ?*const T {
                 const i = iterator.bitset_iterator.next() orelse return null;
@@ -65,17 +68,19 @@ pub fn IOPSType(comptime T: type, comptime size: u8) type {
             }
         };
 
+        /// Iteratos over all currenty executiong IOPs.
         pub fn iterate(self: *IOPS) Iterator {
             return .{
                 .iops = self,
-                .bitset_iterator = self.free.iterator(.{ .kind = .unset }),
+                .bitset_iterator = self.busy.iterate(),
             };
         }
 
+        /// Iteratos over all currenty executiong IOPs.
         pub fn iterate_const(self: *const IOPS) IteratorConst {
             return .{
                 .iops = self,
-                .bitset_iterator = self.free.iterator(.{ .kind = .unset }),
+                .bitset_iterator = self.busy.iterate(),
             };
         }
     };
diff --git a/src/lsm/forest.zig b/src/lsm/forest.zig
index fb391765f9..64d6f66f51 100644
--- a/src/lsm/forest.zig
+++ b/src/lsm/forest.zig
@@ -138,17 +138,17 @@ pub fn ForestType(comptime _Storage: type, comptime groove_cfg: anytype) type {
         for (tree_infos) |tree_info| tree_id_min = @min(tree_id_min, tree_info.tree_id);
 
         var tree_infos_sorted: [tree_infos.len]TreeInfo = undefined;
-        var tree_infos_set = std.StaticBitSet(tree_infos.len).initEmpty();
+        var tree_infos_set: stdx.BitSetType(tree_infos.len) = .{};
         for (tree_infos) |tree_info| {
             const tree_index = tree_info.tree_id - tree_id_min;
-            assert(!tree_infos_set.isSet(tree_index));
+            assert(!tree_infos_set.is_set(tree_index));
 
             tree_infos_sorted[tree_index] = tree_info;
             tree_infos_set.set(tree_index);
         }
 
         // There are no gaps in the tree ids.
-        assert(tree_infos_set.count() == tree_infos.len);
+        assert(tree_infos_set.full());
 
         break :tree_infos tree_infos_sorted;
     };
diff --git a/src/shell.zig b/src/shell.zig
index 1a37203493..5986e297db 100644
--- a/src/shell.zig
+++ b/src/shell.zig
@@ -693,7 +693,7 @@ fn expand_argv(argv: *Argv, comptime cmd: []const u8, cmd_args: anytype) !void {
     comptime var concat_right: bool = false;
 
     const arg_count = std.meta.fields(@TypeOf(cmd_args)).len;
-    comptime var args_used = std.StaticBitSet(arg_count).initEmpty();
+    comptime var args_used: stdx.BitSetType(arg_count) = .{};
     comptime assert(std.mem.indexOfScalar(u8, cmd, '\'') == null); // Quoting isn't supported yet.
     comptime assert(std.mem.indexOfScalar(u8, cmd, '"') == null);
     inline while (pos < cmd.len) {
diff --git a/src/stdx.zig b/src/stdx.zig
index c824534618..aeb84e237d 100644
--- a/src/stdx.zig
+++ b/src/stdx.zig
@@ -6,6 +6,7 @@ const assert = std.debug.assert;
 
 pub const BoundedArrayType = @import("./stdx/bounded_array.zig").BoundedArrayType;
 pub const RingBufferType = @import("./stdx/ring_buffer.zig").RingBufferType;
+pub const BitSetType = @import("./stdx/bit_set.zig").BitSetType;
 pub const ZipfianGenerator = @import("./stdx/zipfian.zig").ZipfianGenerator;
 pub const ZipfianShuffled = @import("./stdx/zipfian.zig").ZipfianShuffled;
 
diff --git a/src/stdx/bit_set.zig b/src/stdx/bit_set.zig
new file mode 100644
index 0000000000..6878164a76
--- /dev/null
+++ b/src/stdx/bit_set.zig
@@ -0,0 +1,156 @@
+const stdx = @import("../stdx.zig");
+const std = @import("std");
+const assert = std.debug.assert;
+
+/// Take a u8 to limit to 255 items max (2^8 - 1 = 255).
+/// Use dynamic bitset for larger sizes.
+pub fn BitSetType(comptime with_capacity: u8) type {
+    return struct {
+        // While mathematicaly 0 and 1 are symmetric, we intentionally bias the API to use zeros
+        // default, as zero-initialization reduces binary size.
+        bits: Word = 0,
+
+        pub const Word = for (.{ u8, u16, u32, u64, u128, u256 }) |w| {
+            if (@bitSizeOf(w) >= with_capacity) break w;
+        } else unreachable;
+
+        const BitSet = @This();
+
+        pub fn is_set(bit_set: BitSet, index: usize) bool {
+            assert(index < bit_set.capacity());
+            return bit_set.bits & bit(index) != 0;
+        }
+
+        pub fn count(bit_set: BitSet) usize {
+            return @popCount(bit_set.bits);
+        }
+
+        pub inline fn capacity(_: BitSet) usize {
+            return with_capacity;
+        }
+
+        pub fn full(bit_set: BitSet) bool {
+            return bit_set.count() == bit_set.capacity();
+        }
+
+        pub fn empty(bit_set: BitSet) bool {
+            return bit_set.bits == 0;
+        }
+
+        pub fn first_set(bit_set: BitSet) ?usize {
+            if (bit_set.bits == 0) return null;
+            return @ctz(bit_set.bits);
+        }
+
+        pub fn first_unset(bit_set: BitSet) ?usize {
+            const result = @ctz(~bit_set.bits);
+            return if (result < bit_set.capacity()) result else null;
+        }
+
+        pub fn set(bit_set: *BitSet, index: usize) void {
+            assert(index < bit_set.capacity());
+            bit_set.bits |= bit(index);
+        }
+
+        pub fn unset(bit_set: *BitSet, index: usize) void {
+            assert(index < bit_set.capacity());
+            bit_set.bits &= ~bit(index);
+        }
+
+        pub fn set_value(bit_set: *BitSet, index: usize, value: bool) void {
+            if (value) {
+                bit_set.set(index);
+            } else {
+                bit_set.unset(index);
+            }
+        }
+
+        fn bit(index: usize) Word {
+            assert(index < with_capacity);
+            return @as(Word, 1) << @intCast(index);
+        }
+
+        pub fn iterate(bit_set: BitSet) Iterator {
+            return .{ .bits_remain = bit_set.bits };
+        }
+
+        pub const Iterator = struct {
+            bits_remain: Word,
+
+            pub fn next(it: *@This()) ?usize {
+                const result = @ctz(it.bits_remain);
+                if (result >= with_capacity) return null;
+                it.bits_remain &= it.bits_remain - 1;
+                return result;
+            }
+        };
+    };
+}
+
+test BitSetType {
+    var prng = stdx.PRNG.from_seed(92);
+    inline for (.{ 0, 1, 8, 32, 65, 255 }) |N| {
+        const BitSet = BitSetType(N);
+
+        var set: BitSet = .{};
+        var model = try std.DynamicBitSetUnmanaged.initEmpty(std.testing.allocator, N);
+        defer model.deinit(std.testing.allocator);
+
+        for (0..1000) |_| {
+            switch (prng.enum_uniform(std.meta.DeclEnum(BitSet))) {
+                .Word => {
+                    const bit_size =
+                        comptime if (N == 0) 8 else @max(8, try std.math.ceilPowerOfTwo(u16, N));
+                    assert(BitSet.Word == std.meta.Int(.unsigned, bit_size));
+                },
+                .Iterator => {},
+                .is_set => {
+                    if (N > 0) {
+                        const bit = prng.int_inclusive(usize, N - 1);
+                        assert(set.is_set(bit) == model.isSet(bit));
+                    }
+                },
+                .count => assert(set.count() == model.count()),
+                .capacity => assert(set.capacity() == N),
+                .full => assert(set.full() == (model.count() == N)),
+                .empty => assert(set.empty() == (model.count() == 0)),
+                .first_set => assert(set.first_set() == model.findFirstSet()),
+                .first_unset => {
+                    var it = model.iterator(.{ .kind = .unset });
+                    assert(set.first_unset() == it.next());
+                },
+                .set => {
+                    if (N > 0) {
+                        const bit = prng.int_inclusive(usize, N - 1);
+                        set.set(bit);
+                        model.set(bit);
+                    }
+                },
+                .unset => {
+                    if (N > 0) {
+                        const bit = prng.int_inclusive(usize, N - 1);
+                        set.unset(bit);
+                        model.unset(bit);
+                    }
+                },
+                .set_value => {
+                    if (N > 0) {
+                        const bit = prng.int_inclusive(usize, N - 1);
+                        const value = prng.boolean();
+                        set.set_value(bit, value);
+                        model.setValue(bit, value);
+                    }
+                },
+                .iterate => {
+                    var it_set = set.iterate();
+                    var it_model = model.iterator(.{});
+                    while (it_model.next()) |next| {
+                        assert(next == it_set.next());
+                    }
+                    assert(it_set.next() == null);
+                    assert(it_set.next() == null);
+                },
+            }
+        }
+    }
+}
diff --git a/src/stdx/prng.zig b/src/stdx/prng.zig
index abf58dc2f5..c8ecf3160b 100644
--- a/src/stdx/prng.zig
+++ b/src/stdx/prng.zig
@@ -15,6 +15,7 @@
 //! - remove dynamic-dispatch idirection (a minor bonus).
 
 const std = @import("std");
+const stdx = @import("../stdx.zig");
 const assert = std.debug.assert;
 const math = std.math;
 const Snap = @import("../testing/snaptest.zig").Snap;
@@ -130,7 +131,7 @@ test fill {
     for (0..size_max + 1) |size| {
         // Check that the entire buffer is filled, by filling it over a couple of times
         // and checking that each byte is non-zero at least once.
-        var non_zero = std.StaticBitSet(size_max).initEmpty();
+        var non_zero: stdx.BitSetType(size_max) = .{};
         for (0..3) |_| {
             const buffer = buffer_max[0..size];
             @memset(buffer, 0);
@@ -140,7 +141,7 @@ test fill {
                 if (byte != 0) non_zero.set(i);
             }
         }
-        for (0..size) |i| assert(non_zero.isSet(i));
+        for (0..size) |i| assert(non_zero.is_set(i));
     }
 
     try snap(@src(),
diff --git a/src/testing/cluster/network.zig b/src/testing/cluster/network.zig
index 2b31834218..c2644f9dbc 100644
--- a/src/testing/cluster/network.zig
+++ b/src/testing/cluster/network.zig
@@ -56,7 +56,7 @@ pub const Network = struct {
     ///
     /// At the moment, we require core members to have direct bidirectional connectivity, but this
     /// could be relaxed in the future to indirect connectivity.
-    pub const Core = std.StaticBitSet(constants.members_max);
+    pub const Core = stdx.BitSetType(constants.members_max);
 
     allocator: std.mem.Allocator,
 
@@ -141,9 +141,9 @@ pub const Network = struct {
         network.packet_simulator.options.partition_probability = ratio(0, 100);
         network.packet_simulator.options.unpartition_probability = ratio(0, 100);
 
-        var it_source = core.iterator(.{});
+        var it_source = core.iterate();
         while (it_source.next()) |replica_source| {
-            var it_target = core.iterator(.{});
+            var it_target = core.iterate();
             while (it_target.next()) |replica_target| {
                 if (replica_target != replica_source) {
                     const path = Path{
diff --git a/src/testing/cluster/state_checker.zig b/src/testing/cluster/state_checker.zig
index 74ae58c267..041ff69ad0 100644
--- a/src/testing/cluster/state_checker.zig
+++ b/src/testing/cluster/state_checker.zig
@@ -4,17 +4,18 @@ const mem = std.mem;
 
 const constants = @import("../../constants.zig");
 const vsr = @import("../../vsr.zig");
+const stdx = @import("../../stdx.zig");
 
 const message_pool = @import("../../message_pool.zig");
 const MessagePool = message_pool.MessagePool;
 const Message = MessagePool.Message;
 
-const ReplicaSet = std.StaticBitSet(constants.members_max);
+const ReplicaSet = stdx.BitSetType(constants.members_max);
 const Commits = std.ArrayList(struct {
     header: vsr.Header.Prepare,
     // null for operation=root and operation=upgrade
     release: ?vsr.Release,
-    replicas: ReplicaSet = ReplicaSet.initEmpty(),
+    replicas: ReplicaSet = .{},
 });
 
 const ReplicaHead = struct {
@@ -53,7 +54,7 @@ pub fn StateCheckerType(comptime Client: type, comptime Replica: type) type {
             var commits = Commits.init(allocator);
             errdefer commits.deinit();
 
-            var commit_replicas = ReplicaSet.initEmpty();
+            var commit_replicas: ReplicaSet = .{};
             for (options.replicas, 0..) |_, i| commit_replicas.set(i);
             try commits.append(.{
                 .header = root_prepare,
diff --git a/src/testing/storage.zig b/src/testing/storage.zig
index e6386ec4c9..7f54077451 100644
--- a/src/testing/storage.zig
+++ b/src/testing/storage.zig
@@ -1024,7 +1024,7 @@ pub const ClusterFaultAtlas = struct {
         faulty_grid: bool,
     };
 
-    const ReplicaSet = std.StaticBitSet(constants.replicas_max);
+    const ReplicaSet = stdx.BitSetType(constants.replicas_max);
     const headers_per_sector = @divExact(constants.sector_size, @sizeOf(vsr.Header));
     const members_max = constants.members_max;
 
@@ -1090,7 +1090,7 @@ pub const ClusterFaultAtlas = struct {
             if (!faulty) continue;
 
             for (0..chunks[0].bit_length) |chunk| {
-                var replicas = ReplicaSet.initEmpty();
+                var replicas: ReplicaSet = .{};
                 while (replicas.count() < faults_max) {
                     const replica_index = prng.int_inclusive(u8, replica_count - 1);
                     if (chunks[replica_index].count() + 1 <
diff --git a/src/tidy.zig b/src/tidy.zig
index cefacbf099..49cf979908 100644
--- a/src/tidy.zig
+++ b/src/tidy.zig
@@ -126,6 +126,10 @@ fn tidy_banned(source: []const u8) ?[]const u8 {
         return "use stdx.BoundedArrayType instead of std version";
     }
 
+    if (std.mem.indexOf(u8, source, "Static" ++ "BitSet") != null) {
+        return "use stdx.BitSetType instead of std version";
+    }
+
     if (std.mem.indexOf(u8, source, "std.time." ++ "Duration") != null) {
         return "use stdx.Duration instead of std veresion";
     }
diff --git a/src/tigerbeetle.zig b/src/tigerbeetle.zig
index 0c87661107..89abe14bc7 100644
--- a/src/tigerbeetle.zig
+++ b/src/tigerbeetle.zig
@@ -187,19 +187,19 @@ pub const CreateAccountResult = enum(u32) {
 
     comptime {
         const values = std.enums.values(CreateAccountResult);
-        const BitSet = std.StaticBitSet(values.len);
-        var set = BitSet.initEmpty();
+        const BitSet = stdx.BitSetType(values.len);
+        var set: BitSet = .{};
         for (0..values.len) |index| {
             const result: CreateAccountResult = @enumFromInt(index);
             stdx.maybe(result == values[index]);
 
-            assert(!set.isSet(index));
+            assert(!set.is_set(index));
             set.set(index);
         }
 
         // It's a non-ordered enum, we need to ensure
         // there are no gaps in the numbering of the values.
-        assert(set.count() == set.capacity());
+        assert(set.full());
     }
 };
 
@@ -385,19 +385,19 @@ pub const CreateTransferResult = enum(u32) {
 
     comptime {
         const values = std.enums.values(CreateTransferResult);
-        const BitSet = std.StaticBitSet(values.len);
-        var set = BitSet.initEmpty();
+        const BitSet = stdx.BitSetType(values.len);
+        var set: BitSet = .{};
         for (0..values.len) |index| {
             const result: CreateTransferResult = @enumFromInt(index);
             stdx.maybe(result == values[index]);
 
-            assert(!set.isSet(index));
+            assert(!set.is_set(index));
             set.set(index);
         }
 
         // It's a non-ordered enum, we need to ensure
         // there are no gaps in the numbering of the values.
-        assert(set.count() == set.capacity());
+        assert(set.full());
     }
 
     /// TODO(zig): CreateTransferResult is ordered by precedence, but it crashes
diff --git a/src/tigerbeetle/benchmark_load.zig b/src/tigerbeetle/benchmark_load.zig
index 07c8be2f8a..8364af0b23 100644
--- a/src/tigerbeetle/benchmark_load.zig
+++ b/src/tigerbeetle/benchmark_load.zig
@@ -268,8 +268,7 @@ const Benchmark = struct {
     print_batch_timings: bool,
 
     // State:
-    clients_busy: std.StaticBitSet(constants.clients_max) =
-        std.StaticBitSet(constants.clients_max).initEmpty(),
+    clients_busy: stdx.BitSetType(constants.clients_max) = .{},
     clients_request_ns: [constants.clients_max]u64 = .{undefined} ** constants.clients_max,
     client_requests: []align(constants.sector_size) [constants.message_body_size_max]u8,
     client_replies: []align(constants.sector_size) [constants.message_body_size_max]u8,
@@ -294,7 +293,7 @@ const Benchmark = struct {
     pub fn run(b: *Benchmark, stage: Stage) !void {
         assert(b.stage == .idle);
         assert(b.clients.len > 0);
-        assert(b.clients_busy.count() == 0);
+        assert(b.clients_busy.empty());
         assert(stdx.zeroed(std.mem.sliceAsBytes(b.request_latency_histogram)));
         assert(b.request_index == 0);
         assert(b.account_index == 0);
@@ -326,7 +325,7 @@ const Benchmark = struct {
 
     fn run_finish(b: *Benchmark) void {
         assert(b.stage != .idle);
-        assert(b.clients_busy.count() == 0);
+        assert(b.clients_busy.empty());
 
         b.stage = .idle;
         b.request_index = 0;
@@ -338,7 +337,7 @@ const Benchmark = struct {
 
     fn register(b: *Benchmark, client_index: usize) void {
         assert(b.stage == .register);
-        assert(!b.clients_busy.isSet(client_index));
+        assert(!b.clients_busy.is_set(client_index));
 
         b.clients_busy.set(client_index);
         b.clients[client_index].register(register_callback, @bitCast(RequestContext{
@@ -353,19 +352,19 @@ const Benchmark = struct {
         const context: RequestContext = @bitCast(user_data);
         const b: *Benchmark = context.benchmark;
         assert(b.stage == .register);
-        assert(b.clients_busy.isSet(context.client_index));
+        assert(b.clients_busy.is_set(context.client_index));
 
         b.clients_busy.unset(context.client_index);
-        if (b.clients_busy.count() == 0) b.run_finish();
+        if (b.clients_busy.empty()) b.run_finish();
     }
 
     fn create_accounts(b: *Benchmark, client_index: u32) void {
         assert(b.stage == .create_accounts);
-        assert(!b.clients_busy.isSet(client_index));
+        assert(!b.clients_busy.is_set(client_index));
         assert(b.account_batch_size > 0);
 
         if (b.account_index >= b.account_count) {
-            if (b.clients_busy.count() == 0) b.run_finish();
+            if (b.clients_busy.empty()) b.run_finish();
         } else {
             const accounts_count = @min(b.account_count, b.account_batch_size);
             const accounts_bytes = &b.client_requests[client_index];
@@ -387,11 +386,11 @@ const Benchmark = struct {
 
     fn create_transfers(b: *Benchmark, client_index: u32) void {
         assert(b.stage == .create_transfers);
-        assert(!b.clients_busy.isSet(client_index));
+        assert(!b.clients_busy.is_set(client_index));
         assert(b.transfer_batch_size > 0);
 
         if (b.transfer_index >= b.transfer_count) {
-            if (b.clients_busy.count() == 0) b.create_transfers_finish();
+            if (b.clients_busy.empty()) b.create_transfers_finish();
         } else {
             const transfers_count = @min(b.transfer_count, b.transfer_batch_size);
             const transfers_bytes = &b.client_requests[client_index];
@@ -449,10 +448,10 @@ const Benchmark = struct {
 
     fn get_account_transfers(b: *Benchmark, client_index: u32) void {
         assert(b.stage == .get_account_transfers);
-        assert(!b.clients_busy.isSet(client_index));
+        assert(!b.clients_busy.is_set(client_index));
 
         if (b.query_index >= b.query_count) {
-            if (b.clients_busy.count() == 0) b.get_account_transfers_finish();
+            if (b.clients_busy.empty()) b.get_account_transfers_finish();
             return;
         }
         b.query_index += 1;
@@ -508,10 +507,10 @@ const Benchmark = struct {
 
     fn validate_accounts(b: *Benchmark, client_index: u32) void {
         assert(b.stage == .validate_accounts);
-        assert(!b.clients_busy.isSet(client_index));
+        assert(!b.clients_busy.is_set(client_index));
 
         if (b.account_index >= b.account_count) {
-            if (b.clients_busy.count() == 0) b.validate_accounts_finish();
+            if (b.clients_busy.empty()) b.validate_accounts_finish();
         } else {
             const account_count = @min(b.account_count, b.account_batch_size);
             const account_ids =
@@ -560,10 +559,10 @@ const Benchmark = struct {
 
     fn validate_transfers(b: *Benchmark, client_index: u32) void {
         assert(b.stage == .validate_transfers);
-        assert(!b.clients_busy.isSet(client_index));
+        assert(!b.clients_busy.is_set(client_index));
 
         if (b.transfer_index >= b.transfer_count) {
-            if (b.clients_busy.count() == 0) b.validate_transfers_finish();
+            if (b.clients_busy.empty()) b.validate_transfers_finish();
         } else {
             const transfer_count = @min(b.transfer_count, b.transfer_batch_size);
             const transfer_ids =
@@ -635,7 +634,7 @@ const Benchmark = struct {
     ) void {
         assert(b.stage != .idle);
         assert(b.clients_busy.count() < b.clients.len);
-        assert(!b.clients_busy.isSet(client_index));
+        assert(!b.clients_busy.is_set(client_index));
 
         b.clients_busy.set(client_index);
         b.clients_request_ns[client_index] = b.timer.read();
@@ -662,7 +661,7 @@ const Benchmark = struct {
         const context: RequestContext = @bitCast(user_data);
         const client = context.client_index;
         const b: *Benchmark = context.benchmark;
-        assert(b.clients_busy.isSet(client));
+        assert(b.clients_busy.is_set(client));
         assert(b.stage != .idle);
         assert(timestamp > 0);
 
diff --git a/src/tigerbeetle/inspect.zig b/src/tigerbeetle/inspect.zig
index 71c528b2c4..3a48ea267b 100644
--- a/src/tigerbeetle/inspect.zig
+++ b/src/tigerbeetle/inspect.zig
@@ -411,13 +411,13 @@ const Inspector = struct {
 
             var label_buffer: [128]u8 = undefined;
             for (group_by.groups()) |group| {
-                const header_index = group.findFirstSet().?;
+                const header_index = group.first_set().?;
                 const header = &inspector.superblock_headers[header_index];
                 const header_mark: u8 = if (header_valid[header_index]) '|' else 'X';
 
                 var label_stream = std.io.fixedBufferStream(&label_buffer);
                 for (0..constants.superblock_copies) |j| {
-                    try label_stream.writer().writeByte(if (group.isSet(j)) header_mark else '_');
+                    try label_stream.writer().writeByte(if (group.is_set(j)) header_mark else '_');
                 }
                 try label_stream.writer().writeByte(' ');
                 try label_stream.writer().writeAll(field.name);
@@ -473,14 +473,14 @@ const Inspector = struct {
 
             var label_buffer: [64]u8 = undefined;
             for (group_by.groups()) |group| {
-                const header = header_pair[group.findFirstSet().?];
+                const header = header_pair[group.first_set().?];
                 const header_valid = header.valid_checksum() and
-                    (!group.isSet(1) or wal_prepare_body_valid);
+                    (!group.is_set(1) or wal_prepare_body_valid);
 
                 const mark: u8 = if (header_valid) '|' else 'X';
                 var label_stream = std.io.fixedBufferStream(&label_buffer);
-                try label_stream.writer().writeByte(if (group.isSet(0)) mark else '_');
-                try label_stream.writer().writeByte(if (group.isSet(1)) mark else '_');
+                try label_stream.writer().writeByte(if (group.is_set(0)) mark else '_');
+                try label_stream.writer().writeByte(if (group.is_set(1)) mark else '_');
                 try label_stream.writer().print("{:_>4}: ", .{slot});
 
                 try print_struct(output, label_stream.getWritten(), &.{
@@ -532,10 +532,10 @@ const Inspector = struct {
 
         var label_buffer: [2]u8 = undefined;
         for (group_by.groups()) |group| {
-            const header = copies[group.findFirstSet().?];
+            const header = copies[group.first_set().?];
             const header_mark: u8 = if (header.valid_checksum()) '|' else 'X';
-            label_buffer[0] = if (group.isSet(0)) header_mark else '_';
-            label_buffer[1] = if (group.isSet(1)) header_mark else '_';
+            label_buffer[0] = if (group.is_set(0)) header_mark else '_';
+            label_buffer[1] = if (group.is_set(1)) header_mark else '_';
 
             try print_struct(output, &label_buffer, header);
         }
@@ -580,14 +580,14 @@ const Inspector = struct {
             try output.print("{:_>2}     session={}\n", .{ slot, session });
 
             for (group_by.groups()) |group| {
-                const header_index = group.findFirstSet().?;
+                const header_index = group.first_set().?;
                 const header = copies[header_index];
                 const header_mark: u8 = if (header.valid_checksum()) '|' else 'X';
 
                 var label_stream = std.io.fixedBufferStream(&label_buffer);
                 try label_stream.writer().print("{:_>2}: ", .{slot});
-                try label_stream.writer().writeByte(if (group.isSet(0)) header_mark else '_');
-                try label_stream.writer().writeByte(if (group.isSet(1)) header_mark else '_');
+                try label_stream.writer().writeByte(if (group.is_set(0)) header_mark else '_');
+                try label_stream.writer().writeByte(if (group.is_set(1)) header_mark else '_');
                 try label_stream.writer().writeAll(" header");
                 try print_struct(output, label_stream.getWritten(), header);
             }
@@ -634,10 +634,10 @@ const Inspector = struct {
 
         var label_buffer: [2]u8 = undefined;
         for (group_by.groups()) |group| {
-            const header = copies[group.findFirstSet().?];
+            const header = copies[group.first_set().?];
             const header_mark: u8 = if (header.valid_checksum()) '|' else 'X';
-            label_buffer[0] = if (group.isSet(0)) header_mark else '_';
-            label_buffer[1] = if (group.isSet(1)) header_mark else '_';
+            label_buffer[0] = if (group.is_set(0)) header_mark else '_';
+            label_buffer[1] = if (group.is_set(1)) header_mark else '_';
 
             try print_struct(output, &label_buffer, header);
         }
@@ -958,7 +958,7 @@ const Inspector = struct {
             var quorums = SuperBlockQuorums{};
             const quorum = try quorums.working(&copies, .open);
             if (!quorum.valid) return error.SuperBlockQuorumInvalid;
-            return inspector.superblock_headers[quorum.copies.findFirstSet().?];
+            return inspector.superblock_headers[quorum.copies.first_set().?];
         }
     }
 
@@ -1481,7 +1481,7 @@ fn print_table_info(
 fn GroupByType(comptime count_max: usize) type {
     return struct {
         const GroupBy = @This();
-        const BitSet = std.StaticBitSet(count_max);
+        const BitSet = stdx.BitSetType(count_max);
 
         count: usize = 0,
         checksums: [count_max]?u128 = [_]?u128{null} ** count_max,
@@ -1500,11 +1500,11 @@ fn GroupByType(comptime count_max: usize) type {
 
             var distinct: usize = 0;
             for (&group_by.checksums, 0..) |checksum_a, a| {
-                var matches = BitSet.initEmpty();
+                var matches: BitSet = .{};
                 for (&group_by.checksums, 0..) |checksum_b, b| {
-                    matches.setValue(b, checksum_a.? == checksum_b.?);
+                    matches.set_value(b, checksum_a.? == checksum_b.?);
                 }
-                if (matches.findFirstSet().? == a) {
+                if (matches.first_set().? == a) {
                     group_by.matches[distinct] = matches;
                     distinct += 1;
                 }
diff --git a/src/unit_tests.zig b/src/unit_tests.zig
index 444806bf81..b58e827ad1 100644
--- a/src/unit_tests.zig
+++ b/src/unit_tests.zig
@@ -11,11 +11,12 @@ comptime {
     _ = @import("shell.zig");
     _ = @import("stdx.zig");
     _ = @import("stdx/aegis.zig");
+    _ = @import("stdx/bit_set.zig");
     _ = @import("stdx/bounded_array.zig");
+    _ = @import("stdx/prng.zig");
     _ = @import("stdx/ring_buffer.zig");
     _ = @import("stdx/sort_test.zig");
     _ = @import("stdx/zipfian.zig");
-    _ = @import("stdx/prng.zig");
     _ = @import("storage.zig");
     _ = @import("tidy.zig");
     _ = @import("trace.zig");
diff --git a/src/vopr.zig b/src/vopr.zig
index 75212f439a..04647433fb 100644
--- a/src/vopr.zig
+++ b/src/vopr.zig
@@ -232,7 +232,7 @@ pub fn main() !void {
         // are repaired. After this, all we must be left with are correlated faults.
         {
             var replica: u8 = 0;
-            var core: Core = Core.initEmpty();
+            var core: Core = .{};
             while (replica < simulator.options.cluster.replica_count) : (replica += 1) {
                 core.set(replica);
             }
@@ -277,7 +277,7 @@ pub fn main() !void {
 
         if (simulator.pending()) |reason| {
             if (try simulator.cluster_recoverable(allocator)) {
-                log.info("no liveness, final cluster state (core={b}):", .{simulator.core.mask});
+                log.info("no liveness, final cluster state (core={b}):", .{simulator.core.bits});
                 simulator.cluster.log_cluster();
                 log.err("you can reproduce this failure with seed={}", .{seed});
                 fatal(.liveness, "no state convergence: {s}", .{reason});
@@ -286,7 +286,7 @@ pub fn main() !void {
             const commits = simulator.cluster.state_checker.commits.items;
             const last_checksum = commits[commits.len - 1].header.checksum;
             for (simulator.cluster.aofs, 0..) |*aof, replica_index| {
-                if (simulator.core.isSet(replica_index)) {
+                if (simulator.core.is_set(replica_index)) {
                     try aof.validate(last_checksum);
                 } else {
                     try aof.validate(null);
@@ -604,7 +604,7 @@ pub const Simulator = struct {
     reply_op_next: u64 = 1, // Skip the root op.
 
     /// Fully-connected subgraph of replicas for liveness checking.
-    core: Core = Core.initEmpty(),
+    core: Core = .{},
 
     /// Total number of requests sent, including those that have not been delivered.
     /// Does not include `register` messages.
@@ -697,7 +697,7 @@ pub const Simulator = struct {
         // Even though there are no client requests in progress, the cluster may be upgrading.
         const release_max = simulator.core_release_max();
         for (simulator.cluster.replicas) |*replica| {
-            if (simulator.core.isSet(replica.replica)) {
+            if (simulator.core.is_set(replica.replica)) {
                 // (If down, the replica is waiting to be upgraded.)
                 maybe(simulator.cluster.replica_health[replica.replica] == .down);
 
@@ -706,7 +706,7 @@ pub const Simulator = struct {
         }
 
         for (simulator.cluster.replicas) |*replica| {
-            if (simulator.core.isSet(replica.replica)) {
+            if (simulator.core.is_set(replica.replica)) {
                 if (!simulator.cluster.state_checker.replica_convergence(replica.replica)) {
                     return "pending replica convergence";
                 }
@@ -718,7 +718,7 @@ pub const Simulator = struct {
         // Check whether the replica is still repairing prepares/tables/replies.
         const commit_max: u64 = simulator.cluster.state_checker.commits.items.len - 1;
         for (simulator.cluster.replicas) |*replica| {
-            if (simulator.core.isSet(replica.replica)) {
+            if (simulator.core.is_set(replica.replica)) {
                 for (replica.op_checkpoint() + 1..commit_max + 1) |op| {
                     const header = simulator.cluster.state_checker.header_with_op(op);
                     if (!replica.journal.has_prepare(&header)) return "pending journal";
@@ -733,7 +733,7 @@ pub const Simulator = struct {
         // Expect that all core replicas have arrived at an identical (non-divergent) checkpoint.
         var checkpoint_id: ?u128 = null;
         for (simulator.cluster.replicas) |*replica| {
-            if (simulator.core.isSet(replica.replica)) {
+            if (simulator.core.is_set(replica.replica)) {
                 const replica_checkpoint_id = replica.superblock.working.checkpoint_id();
                 if (checkpoint_id) |id| {
                     assert(checkpoint_id == id);
@@ -784,13 +784,13 @@ pub const Simulator = struct {
     /// See https://tigerbeetle.com/blog/2023-07-06-simulation-testing-for-liveness for broader
     /// context.
     pub fn transition_to_liveness_mode(simulator: *Simulator, core: Core) void {
-        log.debug("transition_to_liveness_mode: core={b}", .{core.mask});
+        log.debug("transition_to_liveness_mode: core={b}", .{core.bits});
         assert(simulator.core.count() == 0);
         defer assert(simulator.core.count() > 0);
 
         simulator.core = core;
 
-        var it = core.iterator(.{});
+        var it = core.iterate();
         while (it.next()) |replica_index| {
             const fault = false;
             if (simulator.cluster.replica_health[replica_index] == .down) {
@@ -826,11 +826,11 @@ pub const Simulator = struct {
         for (simulator.cluster.replicas) |*replica| {
             if (simulator.cluster.replica_health[replica.replica] == .up and
                 replica.status == .normal and replica.primary() and
-                !simulator.core.isSet(replica.replica))
+                !simulator.core.is_set(replica.replica))
             {
                 // `replica` considers itself a primary, check that at least part of the core thinks
                 // so as well.
-                var it = simulator.core.iterator(.{});
+                var it = simulator.core.iterate();
                 while (it.next()) |replica_core_index| {
                     if (simulator.cluster.replicas[replica_core_index].view == replica.view) {
                         return true;
@@ -849,7 +849,7 @@ pub const Simulator = struct {
         var core_replicas: u8 = 0;
         var core_recovering_head: u8 = 0;
         for (simulator.cluster.replicas) |*replica| {
-            if (simulator.core.isSet(replica.replica) and !replica.standby()) {
+            if (simulator.core.is_set(replica.replica) and !replica.standby()) {
                 core_replicas += 1;
                 core_recovering_head += @intFromBool(replica.status == .recovering_head);
             }
@@ -866,7 +866,7 @@ pub const Simulator = struct {
         comptime Replica: type,
         replica: *const Replica,
     ) bool {
-        if (!simulator.core.isSet(replica.replica)) return false;
+        if (!simulator.core.is_set(replica.replica)) return false;
         if (replica.standby()) return false;
         switch (replica.status) {
             .normal => return true,
@@ -934,7 +934,7 @@ pub const Simulator = struct {
             }
         }
 
-        const ReplicaSet = std.StaticBitSet(constants.replicas_max);
+        const ReplicaSet = stdx.BitSetType(constants.replicas_max);
         var replicas_missing_ops = try allocator.alloc(
             ReplicaSet,
             cluster_op_head - cluster_op_repair_min + 1,
@@ -942,7 +942,7 @@ pub const Simulator = struct {
         errdefer allocator.free(replicas_missing_ops);
 
         for (replicas_missing_ops, cluster_op_repair_min..) |*replicas_missing_op, op| {
-            replicas_missing_op.* = ReplicaSet.initEmpty();
+            replicas_missing_op.* = .{};
             const header = blk: {
                 if (op > cluster_commit_max) {
                     const uncommitted_header = uncommitted_headers[op % pipeline_max].?;
@@ -955,7 +955,7 @@ pub const Simulator = struct {
             for (simulator.cluster.replicas) |replica| {
                 // Replicas should be able to repair using any other replica in the core.
                 if (replica.standby()) continue;
-                if (!simulator.core.isSet(replica.replica) or
+                if (!simulator.core.is_set(replica.replica) or
                     !replica.journal.has_prepare(&header))
                 {
                     replicas_missing_op.set(replica.replica);
@@ -1015,7 +1015,7 @@ pub const Simulator = struct {
     ) error{OutOfMemory}!?usize {
         assert(simulator.core.count() > 0);
 
-        const FaultyReplicas = std.StaticBitSet(constants.members_max);
+        const FaultyReplicas = stdx.BitSetType(constants.members_max);
         var blocks_missing = std.AutoArrayHashMap(
             struct { address: u64, checksum: u128 },
             FaultyReplicas,
@@ -1024,7 +1024,7 @@ pub const Simulator = struct {
 
         // Find all blocks that any replica in the core is missing.
         for (simulator.cluster.replicas) |replica| {
-            if (!simulator.core.isSet(replica.replica)) continue;
+            if (!simulator.core.is_set(replica.replica)) continue;
 
             const storage = &simulator.cluster.storages[replica.replica];
 
@@ -1035,7 +1035,7 @@ pub const Simulator = struct {
                     .checksum = faulty_read.checksum,
                 });
 
-                if (!v.found_existing) v.value_ptr.* = FaultyReplicas.initEmpty();
+                if (!v.found_existing) v.value_ptr.* = .{};
                 v.value_ptr.set(replica.replica);
 
                 log.debug("{}: core_missing_blocks: " ++
@@ -1054,7 +1054,7 @@ pub const Simulator = struct {
                     .checksum = fault.value_ptr.checksum,
                 });
 
-                if (!v.found_existing) v.value_ptr.* = FaultyReplicas.initEmpty();
+                if (!v.found_existing) v.value_ptr.* = .{};
                 v.value_ptr.set(replica.replica);
 
                 log.debug("{}: core_missing_blocks: " ++
@@ -1079,9 +1079,9 @@ pub const Simulator = struct {
                 // A replica might actually have the block that it is requesting, but not know.
                 // This can occur after state sync: if we compact and create a table, but then skip
                 // over that table via state sync, we will try to sync the table anyway.
-                if (faulty_replicas.isSet(replica.replica)) continue;
+                if (faulty_replicas.is_set(replica.replica)) continue;
 
-                if (!simulator.core.isSet(replica.replica)) continue;
+                if (!simulator.core.is_set(replica.replica)) continue;
                 if (replica.standby()) continue;
                 if (storage.area_faulty(.{
                     .grid = .{ .address = block_missing.address },
@@ -1131,7 +1131,7 @@ pub const Simulator = struct {
                 const storage_replies = storage.client_replies();
                 const storage_reply = storage_replies[reply_slot];
 
-                if (simulator.core.isSet(replica.replica) and !replica.standby()) {
+                if (simulator.core.is_set(replica.replica) and !replica.standby()) {
                     if (storage_reply.header.checksum == reply.checksum and
                         !storage.area_faulty(.{ .client_replies = .{ .slot = reply_slot } }))
                     {
@@ -1149,7 +1149,7 @@ pub const Simulator = struct {
 
         var release_max: vsr.Release = vsr.Release.zero;
         for (simulator.cluster.replicas) |*replica| {
-            if (simulator.core.isSet(replica.replica)) {
+            if (simulator.core.is_set(replica.replica)) {
                 release_max = release_max.max(replica.release);
                 if (replica.upgrade_release) |release| {
                     release_max = release_max.max(release);
@@ -1360,7 +1360,7 @@ pub const Simulator = struct {
         const restart_upgrade =
             simulator.replica_releases[replica.replica] <
             simulator.replica_releases_limit and
-            (simulator.core.isSet(replica.replica) or
+            (simulator.core.is_set(replica.replica) or
             simulator.prng.chance(simulator.options.replica_release_catchup_probability));
         if (restart_upgrade) simulator.replica_upgrade(replica.replica);
 
@@ -1541,7 +1541,7 @@ fn random_core(prng: *stdx.PRNG, replica_count: u8, standby_count: u8) Core {
     const replica_core_count = prng.range_inclusive(u8, quorum_view_change, replica_count);
     const standby_core_count = prng.range_inclusive(u8, 0, standby_count);
 
-    var result: Core = Core.initEmpty();
+    var result: Core = .{};
 
     var combination = stdx.PRNG.Combination.init(.{
         .total = replica_count,
diff --git a/src/vsr/client_replies.zig b/src/vsr/client_replies.zig
index 6e2946a629..fd6105f901 100644
--- a/src/vsr/client_replies.zig
+++ b/src/vsr/client_replies.zig
@@ -83,8 +83,7 @@ pub fn ClientRepliesType(comptime Storage: type) type {
         writes: IOPSType(Write, constants.client_replies_iops_write_max) = .{},
 
         /// Track which slots have a write currently in progress.
-        writing: std.StaticBitSet(constants.clients_max) =
-            std.StaticBitSet(constants.clients_max).initEmpty(),
+        writing: stdx.BitSetType(constants.clients_max) = .{},
         /// Track which slots hold a corrupt reply, or are otherwise missing the reply
         /// that ClientSessions believes they should hold.
         ///
@@ -92,8 +91,7 @@ pub fn ClientRepliesType(comptime Storage: type) type {
         /// - Set bits must correspond to occupied slots in ClientSessions.
         /// - Set bits must correspond to entries in ClientSessions with
         ///   `header.size > @sizeOf(vsr.Header)`.
-        faulty: std.StaticBitSet(constants.clients_max) =
-            std.StaticBitSet(constants.clients_max).initEmpty(),
+        faulty: stdx.BitSetType(constants.clients_max) = .{},
 
         /// Guard against multiple concurrent writes to the same slot.
         /// Pointers are into `writes`.
@@ -136,8 +134,8 @@ pub fn ClientRepliesType(comptime Storage: type) type {
             client_replies: *const ClientReplies,
             slot: Slot,
         ) bool {
-            return !client_replies.faulty.isSet(slot.index) and
-                !client_replies.writing.isSet(slot.index);
+            return !client_replies.faulty.is_set(slot.index) and
+                !client_replies.writing.is_set(slot.index);
         }
 
         pub fn read_reply_sync(
@@ -147,7 +145,7 @@ pub fn ClientRepliesType(comptime Storage: type) type {
         ) ?*Message.Reply {
             const client = session.header.client;
 
-            if (!client_replies.writing.isSet(slot.index)) return null;
+            if (!client_replies.writing.is_set(slot.index)) return null;
 
             var writes = client_replies.writes.iterate();
             var write_latest: ?*const Write = null;
@@ -171,11 +169,11 @@ pub fn ClientRepliesType(comptime Storage: type) type {
                 // We are writing a reply, but that's a wrong reply according to `client_sessions`.
                 // This happens after state sync, where we update `client_sessions` without
                 // waiting for the in-flight write requests to complete.
-                assert(client_replies.faulty.isSet(slot.index));
+                assert(client_replies.faulty.is_set(slot.index));
                 return null;
             }
 
-            assert(!client_replies.faulty.isSet(slot.index));
+            assert(!client_replies.faulty.is_set(slot.index));
             return write_latest.?.message;
         }
 
@@ -319,7 +317,7 @@ pub fn ClientRepliesType(comptime Storage: type) type {
         }
 
         pub fn remove_reply(client_replies: *ClientReplies, slot: Slot) void {
-            maybe(client_replies.faulty.isSet(slot.index));
+            maybe(client_replies.faulty.is_set(slot.index));
 
             client_replies.faulty.unset(slot.index);
         }
@@ -335,7 +333,7 @@ pub fn ClientRepliesType(comptime Storage: type) type {
             assert(client_replies.ready_sync());
             assert(client_replies.ready_callback == null);
             assert(client_replies.writes.available() > 0);
-            maybe(client_replies.writing.isSet(slot.index));
+            maybe(client_replies.writing.is_set(slot.index));
             assert(client_replies.writing.count() + client_replies.write_queue.count ==
                 client_replies.writes.executing());
             assert(message.header.command == .reply);
@@ -346,11 +344,11 @@ pub fn ClientRepliesType(comptime Storage: type) type {
             switch (trigger) {
                 .commit => {
                     assert(client_replies.checkpoint_callback == null);
-                    maybe(client_replies.faulty.isSet(slot.index));
+                    maybe(client_replies.faulty.is_set(slot.index));
                 },
                 .repair => {
                     maybe(client_replies.checkpoint_callback == null);
-                    assert(client_replies.faulty.isSet(slot.index));
+                    assert(client_replies.faulty.is_set(slot.index));
                 },
             }
 
@@ -403,12 +401,12 @@ pub fn ClientRepliesType(comptime Storage: type) type {
                 client_replies.write_reply_next();
             }
 
-            assert(client_replies.writing.isSet(write.slot.index));
+            assert(client_replies.writing.is_set(write.slot.index));
         }
 
         fn write_reply_next(client_replies: *ClientReplies) void {
             while (client_replies.write_queue.head()) |write| {
-                if (client_replies.writing.isSet(write.slot.index)) return;
+                if (client_replies.writing.is_set(write.slot.index)) return;
 
                 const message = write.message;
                 _ = client_replies.write_queue.pop();
@@ -433,8 +431,8 @@ pub fn ClientRepliesType(comptime Storage: type) type {
             const write: *ClientReplies.Write = @fieldParentPtr("completion", completion);
             const client_replies = write.client_replies;
             const message = write.message;
-            assert(client_replies.writing.isSet(write.slot.index));
-            maybe(client_replies.faulty.isSet(write.slot.index));
+            assert(client_replies.writing.is_set(write.slot.index));
+            maybe(client_replies.faulty.is_set(write.slot.index));
 
             var reads = client_replies.reads.iterate();
             while (reads.next()) |read| {
diff --git a/src/vsr/client_sessions.zig b/src/vsr/client_sessions.zig
index e6ddafa4a7..ddcbd87c25 100644
--- a/src/vsr/client_sessions.zig
+++ b/src/vsr/client_sessions.zig
@@ -40,12 +40,12 @@ pub const ClientSessions = struct {
     /// Values are indexes into `entries`.
     const EntriesByClient = std.AutoHashMapUnmanaged(u128, usize);
 
-    const EntriesFree = std.StaticBitSet(constants.clients_max);
+    const EntriesPresent = stdx.BitSetType(constants.clients_max);
 
     /// Free entries are zeroed, both in `entries` and on-disk.
     entries: []Entry,
     entries_by_client: EntriesByClient,
-    entries_free: EntriesFree = EntriesFree.initFull(),
+    entries_present: EntriesPresent = .{},
 
     pub fn init(allocator: mem.Allocator) !ClientSessions {
         var entries_by_client: EntriesByClient = .{};
@@ -72,7 +72,7 @@ pub const ClientSessions = struct {
     pub fn reset(client_sessions: *ClientSessions) void {
         @memset(client_sessions.entries, std.mem.zeroes(Entry));
         client_sessions.entries_by_client.clearRetainingCapacity();
-        client_sessions.entries_free = EntriesFree.initFull();
+        client_sessions.entries_present = .{};
     }
 
     /// Size of the buffer needed to encode the client sessions on disk.
@@ -136,7 +136,7 @@ pub const ClientSessions = struct {
         source: []align(@alignOf(vsr.Header)) const u8,
     ) void {
         assert(client_sessions.count() == 0);
-        assert(client_sessions.entries_free.count() == constants.clients_max);
+        assert(client_sessions.entries_present.empty());
         for (client_sessions.entries) |*entry| {
             assert(entry.session == 0);
             assert(stdx.zeroed(std.mem.asBytes(&entry.header)));
@@ -174,7 +174,7 @@ pub const ClientSessions = struct {
                 assert(header.commit >= session);
 
                 client_sessions.entries_by_client.putAssumeCapacityNoClobber(header.client, i);
-                client_sessions.entries_free.unset(i);
+                client_sessions.entries_present.set(i);
                 client_sessions.entries[i] = .{
                     .session = session,
                     .header = header.*,
@@ -182,8 +182,9 @@ pub const ClientSessions = struct {
             }
         }
 
-        assert(constants.clients_max - client_sessions.entries_free.count() ==
-            client_sessions.entries_by_client.count());
+        assert(
+            client_sessions.entries_present.count() == client_sessions.entries_by_client.count(),
+        );
     }
 
     pub fn count(client_sessions: *const ClientSessions) usize {
@@ -238,7 +239,7 @@ pub const ClientSessions = struct {
         const entry_gop = client_sessions.entries_by_client.getOrPutAssumeCapacity(client);
         if (entry_gop.found_existing) {
             const entry_index = entry_gop.value_ptr.*;
-            assert(!client_sessions.entries_free.isSet(entry_index));
+            assert(client_sessions.entries_present.is_set(entry_index));
 
             const existing = &client_sessions.entries[entry_index];
             assert(existing.session == session);
@@ -249,8 +250,8 @@ pub const ClientSessions = struct {
             existing.header = header.*;
             return ReplySlot{ .index = entry_index };
         } else {
-            const entry_index = client_sessions.entries_free.findFirstSet().?;
-            client_sessions.entries_free.unset(entry_index);
+            const entry_index = client_sessions.entries_present.first_unset().?;
+            client_sessions.entries_present.set(entry_index);
 
             const e = &client_sessions.entries[entry_index];
             assert(e.session == 0);
@@ -272,7 +273,7 @@ pub const ClientSessions = struct {
     /// This ensures that we will always pick the entry with the oldest commit number.
     /// We also check that a client has only one entry in the hash map (or it's buggy).
     pub fn evictee(client_sessions: *const ClientSessions) u128 {
-        assert(client_sessions.entries_free.count() == 0);
+        assert(client_sessions.entries_present.full());
         assert(client_sessions.count() == constants.clients_max);
 
         var evictee_: ?*const vsr.Header.Reply = null;
@@ -302,8 +303,8 @@ pub const ClientSessions = struct {
     pub fn remove(client_sessions: *ClientSessions, client: u128) void {
         const entry_index = client_sessions.entries_by_client.fetchRemove(client).?.value;
 
-        assert(!client_sessions.entries_free.isSet(entry_index));
-        client_sessions.entries_free.set(entry_index);
+        assert(client_sessions.entries_present.is_set(entry_index));
+        client_sessions.entries_present.unset(entry_index);
 
         assert(client_sessions.entries[entry_index].header.client == client);
         client_sessions.entries[entry_index] = std.mem.zeroes(Entry);
@@ -321,9 +322,9 @@ pub const ClientSessions = struct {
 
                 const entry = &it.client_sessions.entries[it.index];
                 if (entry.session == 0) {
-                    assert(it.client_sessions.entries_free.isSet(it.index));
+                    assert(!it.client_sessions.entries_present.is_set(it.index));
                 } else {
-                    assert(!it.client_sessions.entries_free.isSet(it.index));
+                    assert(it.client_sessions.entries_present.is_set(it.index));
                     return entry;
                 }
             }
diff --git a/src/vsr/journal.zig b/src/vsr/journal.zig
index 0b3e85a5df..e8898fe272 100644
--- a/src/vsr/journal.zig
+++ b/src/vsr/journal.zig
@@ -204,7 +204,7 @@ pub fn JournalType(comptime Replica: type, comptime Storage: type) type {
             }
         };
 
-        const HeaderChunks = std.StaticBitSet(stdx.div_ceil(slot_count, headers_per_message));
+        const HeaderChunks = stdx.BitSetType(stdx.div_ceil(slot_count, headers_per_message));
 
         storage: *Storage,
         replica: u8,
@@ -240,10 +240,10 @@ pub fn JournalType(comptime Replica: type, comptime Storage: type) type {
             constants.journal_iops_write_max
         ][constants.sector_size]u8,
 
-        /// A set bit indicates a chunk of redundant headers that no read has been issued to yet.
-        header_chunks_requested: HeaderChunks = HeaderChunks.initFull(),
+        /// A set bit indicates a chunk of redundant headers for which a read has been issued.
+        header_chunks_requested: HeaderChunks = .{},
         /// A set bit indicates a chunk of redundant headers that has been recovered.
-        header_chunks_recovered: HeaderChunks = HeaderChunks.initEmpty(),
+        header_chunks_recovered: HeaderChunks = .{},
 
         /// Statically allocated read IO operation context data.
         reads: IOPSType(Read, constants.journal_iops_read_max) = .{},
@@ -956,8 +956,8 @@ pub fn JournalType(comptime Replica: type, comptime Storage: type) type {
             assert(journal.faulty.count == slot_count);
             assert(journal.reads.executing() == 0);
             assert(journal.writes.executing() == 0);
-            assert(journal.header_chunks_requested.count() == HeaderChunks.bit_length);
-            assert(journal.header_chunks_recovered.count() == 0);
+            assert(journal.header_chunks_requested.empty());
+            assert(journal.header_chunks_recovered.empty());
 
             journal.status = .{ .recovering = callback };
             log.debug("{}: recover: recovering", .{journal.replica});
@@ -965,25 +965,26 @@ pub fn JournalType(comptime Replica: type, comptime Storage: type) type {
             var available: usize = journal.reads.available();
             while (available > 0) : (available -= 1) journal.recover_headers();
 
-            assert(journal.header_chunks_recovered.count() == 0);
-            assert(journal.header_chunks_requested.count() ==
-                HeaderChunks.bit_length - journal.reads.executing());
+            assert(journal.header_chunks_recovered.empty());
+            assert(journal.header_chunks_requested.count() == journal.reads.executing());
         }
 
         fn recover_headers(journal: *Journal) void {
             const replica: *Replica = @alignCast(@fieldParentPtr("journal", journal));
             assert(journal.status == .recovering);
             assert(journal.reads.available() > 0);
+            assert(
+                journal.header_chunks_recovered.count() <= journal.header_chunks_requested.count(),
+            );
 
-            if (journal.header_chunks_recovered.count() == HeaderChunks.bit_length) {
-                assert(journal.header_chunks_requested.count() == 0);
+            if (journal.header_chunks_recovered.full()) {
                 log.debug("{}: recover_headers: complete", .{journal.replica});
                 journal.recover_prepares();
                 return;
             }
 
-            const chunk_index = journal.header_chunks_requested.findFirstSet() orelse return;
-            assert(!journal.header_chunks_recovered.isSet(chunk_index));
+            const chunk_index = journal.header_chunks_requested.first_unset() orelse return;
+            assert(!journal.header_chunks_recovered.is_set(chunk_index));
 
             const message = replica.message_bus.get_message(.prepare);
             defer replica.message_bus.unref(message);
@@ -1013,7 +1014,7 @@ pub fn JournalType(comptime Replica: type, comptime Storage: type) type {
                 buffer.len,
             });
 
-            journal.header_chunks_requested.unset(chunk_index);
+            journal.header_chunks_requested.set(chunk_index);
             journal.storage.read_sectors(
                 recover_headers_callback,
                 &chunk_read.completion,
@@ -1031,8 +1032,8 @@ pub fn JournalType(comptime Replica: type, comptime Storage: type) type {
             assert(chunk_read.destination_replica == null);
 
             const chunk_index = chunk_read.op;
-            assert(!journal.header_chunks_requested.isSet(chunk_index));
-            assert(!journal.header_chunks_recovered.isSet(chunk_index));
+            assert(journal.header_chunks_requested.is_set(chunk_index));
+            assert(!journal.header_chunks_recovered.is_set(chunk_index));
 
             const chunk_buffer = recover_headers_buffer(
                 chunk_read.message,
@@ -1664,8 +1665,8 @@ pub fn JournalType(comptime Replica: type, comptime Storage: type) type {
             assert(journal.dirty.count <= slot_count);
             assert(journal.faulty.count <= slot_count);
             assert(journal.faulty.count == journal.dirty.count);
-            assert(journal.header_chunks_requested.count() == 0);
-            assert(journal.header_chunks_recovered.count() == HeaderChunks.bit_length);
+            assert(journal.header_chunks_requested.full());
+            assert(journal.header_chunks_recovered.full());
 
             const replica: *Replica = @alignCast(@fieldParentPtr("journal", journal));
             const callback = journal.status.recovering;
diff --git a/src/vsr/replica.zig b/src/vsr/replica.zig
index a8b964d739..bf96bf89f4 100644
--- a/src/vsr/replica.zig
+++ b/src/vsr/replica.zig
@@ -126,8 +126,8 @@ const Request = struct {
 const DVCQuorumMessages = [constants.replicas_max]?*Message.DoViewChange;
 const dvc_quorum_messages_null = [_]?*Message.DoViewChange{null} ** constants.replicas_max;
 
-const QuorumCounter = std.StaticBitSet(constants.replicas_max);
-const quorum_counter_null = QuorumCounter.initEmpty();
+const QuorumCounter = stdx.BitSetType(constants.replicas_max);
+const quorum_counter_null: QuorumCounter = .{};
 
 pub fn ReplicaType(
     comptime StateMachine: type,
@@ -904,7 +904,7 @@ pub fn ReplicaType(
             assert(self.syncing == .idle);
             assert(self.sync_tables == null);
             assert(self.grid_repair_tables.executing() == 0);
-            assert(self.client_sessions.entries_free.count() == constants.clients_max);
+            assert(self.client_sessions.entries_present.empty());
             assert(std.meta.eql(
                 self.client_sessions_checkpoint.checkpoint_reference(),
                 self.superblock.working.client_sessions_reference(),
@@ -927,10 +927,10 @@ pub fn ReplicaType(
             }
 
             if (self.superblock.working.vsr_state.sync_op_max > 0) {
-                maybe(self.client_replies.writing.count() > 0);
+                maybe(!self.client_replies.writing.empty());
                 for (0..constants.clients_max) |entry_slot| {
-                    const slot_faulty = self.client_replies.faulty.isSet(entry_slot);
-                    const slot_free = self.client_sessions.entries_free.isSet(entry_slot);
+                    const slot_faulty = self.client_replies.faulty.is_set(entry_slot);
+                    const slot_free = !self.client_sessions.entries_present.is_set(entry_slot);
                     assert(!slot_faulty);
                     if (!slot_free) {
                         const entry = &self.client_sessions.entries[entry_slot];
@@ -938,7 +938,7 @@ pub fn ReplicaType(
                             entry.header.op <= self.superblock.working.vsr_state.sync_op_max)
                         {
                             const entry_faulty = entry.header.size > @sizeOf(Header);
-                            self.client_replies.faulty.setValue(entry_slot, entry_faulty);
+                            self.client_replies.faulty.set_value(entry_slot, entry_faulty);
                         }
                     }
                 }
@@ -1911,7 +1911,7 @@ pub fn ReplicaType(
             else
                 self.quorum_replication;
 
-            if (!prepare.ok_from_all_replicas.isSet(message.header.replica)) {
+            if (!prepare.ok_from_all_replicas.is_set(message.header.replica)) {
                 self.primary_abdicating = false;
                 if (!prepare.ok_quorum_received) {
                     self.primary_abdicate_timeout.reset();
@@ -1972,7 +1972,7 @@ pub fn ReplicaType(
             }
 
             const slot = self.client_sessions.get_slot_for_header(message.header).?;
-            if (!self.client_replies.faulty.isSet(slot.index)) {
+            if (!self.client_replies.faulty.is_set(slot.index)) {
                 log.debug("{}: on_reply: ignoring, reply is clean (client={} request={})", .{
                     self.replica,
                     message.header.client,
@@ -2158,18 +2158,18 @@ pub fn ReplicaType(
                     self.view,
                     count,
                     threshold,
-                    self.start_view_change_from_all_replicas.mask,
+                    self.start_view_change_from_all_replicas.bits,
                 });
                 return;
             }
             log.debug("{}: on_start_view_change: view={} quorum received (replicas={b:0>6})", .{
                 self.replica,
                 self.view,
-                self.start_view_change_from_all_replicas.mask,
+                self.start_view_change_from_all_replicas.bits,
             });
 
             self.transition_to_view_change_status(self.view + 1);
-            assert(self.start_view_change_from_all_replicas.count() == 0);
+            assert(self.start_view_change_from_all_replicas.empty());
         }
 
         /// DVC serves two purposes:
@@ -3208,7 +3208,7 @@ pub fn ReplicaType(
                     self.replica_count,
                 ));
                 assert(replica != self.replica);
-                if (!prepare.ok_from_all_replicas.isSet(replica)) {
+                if (!prepare.ok_from_all_replicas.is_set(replica)) {
                     waiting[waiting_len] = replica;
                     waiting_len += 1;
                 }
@@ -3216,7 +3216,7 @@ pub fn ReplicaType(
 
             if (waiting_len == 0) {
                 assert(self.quorum_replication == self.replica_count);
-                assert(!prepare.ok_from_all_replicas.isSet(self.replica));
+                assert(!prepare.ok_from_all_replicas.is_set(self.replica));
                 assert(prepare.ok_from_all_replicas.count() == self.replica_count - 1);
                 assert(prepare.message.header.op <= self.op);
 
@@ -3300,14 +3300,14 @@ pub fn ReplicaType(
 
         fn on_start_view_change_window_timeout(self: *Replica) void {
             assert(self.status == .normal or self.status == .view_change);
-            assert(self.start_view_change_from_all_replicas.count() > 0);
+            assert(!self.start_view_change_from_all_replicas.empty());
             assert(!self.solo());
             self.start_view_change_window_timeout.stop();
 
             if (self.standby()) return;
 
             // Don't reset our own SVC; it will be reset if/when we receive a heartbeat.
-            const svc = self.start_view_change_from_all_replicas.isSet(self.replica);
+            const svc = self.start_view_change_from_all_replicas.is_set(self.replica);
             self.reset_quorum_start_view_change();
             if (svc) self.start_view_change_from_all_replicas.set(self.replica);
         }
@@ -3319,7 +3319,7 @@ pub fn ReplicaType(
             if (self.solo()) return;
             if (self.standby()) return;
 
-            if (self.start_view_change_from_all_replicas.isSet(self.replica)) {
+            if (self.start_view_change_from_all_replicas.is_set(self.replica)) {
                 self.send_start_view_change();
             }
         }
@@ -3665,7 +3665,7 @@ pub fn ReplicaType(
             assert(threshold >= 1);
             assert(threshold <= self.replica_count);
 
-            assert(QuorumCounter.bit_length == constants.replicas_max);
+            assert(counter.capacity() == constants.replicas_max);
             assert(message.header.cluster == self.cluster);
             assert(message.header.replica < self.replica_count);
             assert(message.header.view == self.view);
@@ -3684,7 +3684,7 @@ pub fn ReplicaType(
 
             // Do not allow duplicate messages to trigger multiple passes through a state
             // transition:
-            if (counter.isSet(message.header.replica)) {
+            if (counter.is_set(message.header.replica)) {
                 log.debug("{}: on_{s}: ignoring (duplicate message replica={})", .{
                     self.replica,
                     command,
@@ -3695,7 +3695,7 @@ pub fn ReplicaType(
 
             // Record the first receipt of this message:
             counter.set(message.header.replica);
-            assert(counter.isSet(message.header.replica));
+            assert(counter.is_set(message.header.replica));
 
             // Count the number of unique messages now received:
             const count = counter.count();
@@ -6840,10 +6840,10 @@ pub fn ReplicaType(
                 self.commit_journal();
             }
 
-            if (self.client_replies.faulty.findFirstSet()) |slot| {
+            if (self.client_replies.faulty.first_set()) |slot| {
                 // Repair replies.
                 const entry = &self.client_sessions.entries[slot];
-                assert(!self.client_sessions.entries_free.isSet(slot));
+                assert(self.client_sessions.entries_present.is_set(slot));
                 assert(entry.session != 0);
                 assert(entry.header.size > @sizeOf(Header));
 
@@ -7755,17 +7755,17 @@ pub fn ReplicaType(
         }
 
         fn reset_quorum_counter(self: *Replica, counter: *QuorumCounter) void {
-            var counter_iterator = counter.iterator(.{});
+            var counter_iterator = counter.iterate();
             while (counter_iterator.next()) |replica| {
                 assert(replica < self.replica_count);
             }
 
             counter.* = quorum_counter_null;
-            assert(counter.count() == 0);
+            assert(counter.empty());
 
             var replica: usize = 0;
             while (replica < self.replica_count) : (replica += 1) {
-                assert(!counter.isSet(replica));
+                assert(!counter.is_set(replica));
             }
         }
 
@@ -7934,7 +7934,7 @@ pub fn ReplicaType(
 
             self.send_header_to_other_replicas(header.frame_const().*);
 
-            if (!self.start_view_change_from_all_replicas.isSet(self.replica)) {
+            if (!self.start_view_change_from_all_replicas.is_set(self.replica)) {
                 self.send_header_to_replica(self.replica, header.frame_const().*);
                 return self.flush_loopback_queue();
             }
@@ -7954,10 +7954,10 @@ pub fn ReplicaType(
             assert(self.view_headers.array.get(0).op >= self.op);
             self.view_headers.verify();
 
-            const BitSet = std.bit_set.IntegerBitSet(128);
-            comptime assert(BitSet.MaskInt ==
+            const BitSet = stdx.BitSetType(128);
+            comptime assert(BitSet.Word ==
                 std.meta.fieldInfo(Header.DoViewChange, .present_bitset).type);
-            comptime assert(BitSet.MaskInt ==
+            comptime assert(BitSet.Word ==
                 std.meta.fieldInfo(Header.DoViewChange, .nack_bitset).type);
 
             // Collect nack and presence bits for the headers, so that the new primary can run CTRL
@@ -7966,8 +7966,8 @@ pub fn ReplicaType(
             // - a header isn't truncated and is present -- the header gets into the next view
             // - a header is neither truncated nor present -- the primary waits for more
             //   DVC messages to decide whether to keep or truncate the header.
-            var nacks = BitSet.initEmpty();
-            var present = BitSet.initEmpty();
+            var nacks: BitSet = .{};
+            var present: BitSet = .{};
             for (self.view_headers.array.const_slice(), 0..) |*header, i| {
                 const slot = self.journal.slot_for_op(header.op);
                 const journal_header = self.journal.header_with_op(header.op);
@@ -8007,13 +8007,13 @@ pub fn ReplicaType(
                         if (journal_header != null) {
                             assert(journal_header.?.checksum == header.checksum);
                         }
-                        maybe(nacks.isSet(i));
+                        maybe(nacks.is_set(i));
                         present.set(i);
                     }
                 } else {
                     assert(vsr.Headers.dvc_header_type(header) == .blank);
-                    assert(!present.isSet(i));
-                    if (nacks.isSet(i)) assert(!faulty);
+                    assert(!present.is_set(i));
+                    if (nacks.is_set(i)) assert(!faulty);
                 }
             }
 
@@ -8040,9 +8040,9 @@ pub fn ReplicaType(
                 // from non-canonical DVCs.
                 .commit_min = self.commit_min,
                 // Signal which headers correspond to definitely not-prepared messages.
-                .nack_bitset = nacks.mask,
+                .nack_bitset = nacks.bits,
                 // Signal which headers correspond to locally available prepares.
-                .present_bitset = present.mask,
+                .present_bitset = present.bits,
             };
 
             stdx.copy_disjoint(
@@ -8872,10 +8872,10 @@ pub fn ReplicaType(
                     },
                 );
 
-                const BitSet = std.bit_set.IntegerBitSet(128);
+                const BitSet = stdx.BitSetType(128);
                 const dvc_headers = message_body_as_view_headers(dvc.base_const());
-                const dvc_nacks = BitSet{ .mask = dvc.header.nack_bitset };
-                const dvc_present = BitSet{ .mask = dvc.header.present_bitset };
+                const dvc_nacks = BitSet{ .bits = dvc.header.nack_bitset };
+                const dvc_present = BitSet{ .bits = dvc.header.present_bitset };
                 for (dvc_headers.slice, 0..) |*header, i| {
                     log.debug("{}: {s}: dvc: header: " ++
                         "replica={} op={} checksum={} nack={} present={} type={s}", .{
@@ -8884,8 +8884,8 @@ pub fn ReplicaType(
                         dvc.header.replica,
                         header.op,
                         header.checksum,
-                        dvc_nacks.isSet(i),
-                        dvc_present.isSet(i),
+                        dvc_nacks.is_set(i),
+                        dvc_present.is_set(i),
                         @tagName(vsr.Headers.dvc_header_type(header)),
                     });
                 }
@@ -8919,7 +8919,7 @@ pub fn ReplicaType(
                 while (pipeline_prepares.next()) |prepare| {
                     assert(self.journal.has_header(prepare.message.header));
                     assert(!prepare.ok_quorum_received);
-                    assert(prepare.ok_from_all_replicas.count() == 0);
+                    assert(prepare.ok_from_all_replicas.empty());
 
                     log.debug("{}: start_view_as_the_new_primary: pipeline " ++
                         "(op={} checksum={x} parent={x})", .{
@@ -9532,7 +9532,7 @@ pub fn ReplicaType(
             self.client_sessions_checkpoint.reset();
             self.client_sessions.reset();
             // Faulty bits will be set in sync_content().
-            while (self.client_replies.faulty.findFirstSet()) |slot| {
+            while (self.client_replies.faulty.first_set()) |slot| {
                 self.client_replies.faulty.unset(slot);
             }
         }
@@ -9663,7 +9663,7 @@ pub fn ReplicaType(
                 if (!self.state_machine_opened) return false;
 
                 for (0..constants.clients_max) |entry_slot| {
-                    if (self.client_sessions.entries_free.isSet(entry_slot)) continue;
+                    if (!self.client_sessions.entries_present.is_set(entry_slot)) continue;
 
                     const entry = &self.client_sessions.entries[entry_slot];
                     if (entry.header.op >= self.superblock.working.vsr_state.sync_op_min and
@@ -10711,21 +10711,21 @@ const DVCQuorum = struct {
                 assert(header.op == op);
                 assert(header.view <= log_view_canonical);
 
-                const header_nacks = std.bit_set.IntegerBitSet(128){
-                    .mask = dvc.header.nack_bitset,
+                const header_nacks = stdx.BitSetType(128){
+                    .bits = dvc.header.nack_bitset,
                 };
-                const header_present = std.bit_set.IntegerBitSet(128){
-                    .mask = dvc.header.present_bitset,
+                const header_present = stdx.BitSetType(128){
+                    .bits = dvc.header.present_bitset,
                 };
 
                 if (vsr.Headers.dvc_header_type(header) == .valid and
-                    header_present.isSet(header_index) and
+                    header_present.is_set(header_index) and
                     header_canonical != null and header_canonical.?.checksum == header.checksum)
                 {
                     copies += 1;
                 }
 
-                if (header_nacks.isSet(header_index)) {
+                if (header_nacks.is_set(header_index)) {
                     // The op is nacked explicitly.
                     nacks += 1;
                 } else if (vsr.Headers.dvc_header_type(header) == .valid) {
diff --git a/src/vsr/superblock_quorums.zig b/src/vsr/superblock_quorums.zig
index b7ed5d0303..f78ecec053 100644
--- a/src/vsr/superblock_quorums.zig
+++ b/src/vsr/superblock_quorums.zig
@@ -21,7 +21,7 @@ pub fn QuorumsType(comptime options: Options) type {
             valid: bool = false,
             /// Track which copies are a member of the quorum.
             /// Used to ignore duplicate copies of a header when determining a quorum.
-            copies: QuorumCount = QuorumCount.initEmpty(),
+            copies: QuorumCount = .{},
             /// An integer value indicates the copy index found in the corresponding slot.
             /// A `null` value indicates that the copy is invalid or not a member of the working
             /// quorum. All copies belong to the same (valid, working) quorum.
@@ -33,7 +33,7 @@ pub fn QuorumsType(comptime options: Options) type {
             }
         };
 
-        pub const QuorumCount = std.StaticBitSet(options.superblock_copies);
+        pub const QuorumCount = stdx.BitSetType(options.superblock_copies);
 
         pub const Error = error{
             Fork,
@@ -102,7 +102,7 @@ pub fn QuorumsType(comptime options: Options) type {
             std.mem.sort(Quorum, quorums.slice(), {}, sort_priority_descending);
 
             for (quorums.slice()) |quorum| {
-                if (quorum.copies.count() == options.superblock_copies) {
+                if (quorum.copies.full()) {
                     log.debug("quorum: checksum={x} parent={x} sequence={} count={} valid={}", .{
                         quorum.header.checksum,
                         quorum.header.parent,
@@ -254,7 +254,7 @@ pub fn QuorumsType(comptime options: Options) type {
                 // that is, the copy is in the correct slot, and its copy index is simply corrupt.
                 quorum.slots[slot] = @intCast(slot);
                 quorum.copies.set(slot);
-            } else if (quorum.copies.isSet(copy.copy)) {
+            } else if (quorum.copies.is_set(copy.copy)) {
                 // Ignore the duplicate copy.
             } else {
                 quorum.slots[slot] = @intCast(copy.copy);
@@ -337,13 +337,13 @@ pub fn QuorumsType(comptime options: Options) type {
                 }
 
                 // Set bits indicate that the corresponding copy was found at least once.
-                var copies_any = QuorumCount.initEmpty();
+                var copies_any: QuorumCount = .{};
                 // Set bits indicate that the corresponding copy was found more than once.
-                var copies_duplicate = QuorumCount.initEmpty();
+                var copies_duplicate: QuorumCount = .{};
 
                 for (iterator.slots) |slot| {
                     if (slot) |copy| {
-                        if (copies_any.isSet(copy)) copies_duplicate.set(copy);
+                        if (copies_any.is_set(copy)) copies_duplicate.set(copy);
                         copies_any.set(copy);
                     }
                 }
@@ -356,10 +356,10 @@ pub fn QuorumsType(comptime options: Options) type {
                 var b: ?u8 = null;
                 var c: ?u8 = null;
                 for (iterator.slots, 0..) |slot, i| {
-                    if (slot == null and !copies_any.isSet(i)) a = @intCast(i);
-                    if (slot == null and copies_any.isSet(i)) b = @intCast(i);
+                    if (slot == null and !copies_any.is_set(i)) a = @intCast(i);
+                    if (slot == null and copies_any.is_set(i)) b = @intCast(i);
                     if (slot) |slot_copy| {
-                        if (slot_copy != i and copies_duplicate.isSet(slot_copy)) c = @intCast(i);
+                        if (slot_copy != i and copies_duplicate.is_set(slot_copy)) c = @intCast(i);
                     }
                 }
 
diff --git a/src/vsr/superblock_quorums_fuzz.zig b/src/vsr/superblock_quorums_fuzz.zig
index 96d6d08da6..ebc55bd431 100644
--- a/src/vsr/superblock_quorums_fuzz.zig
+++ b/src/vsr/superblock_quorums_fuzz.zig
@@ -314,14 +314,14 @@ pub fn fuzz_quorum_repairs(
     // Generate a random valid 2/4 quorum.
     // 1 bits indicate valid headers.
     // 0 bits indicate invalid headers.
-    var valid = std.bit_set.IntegerBitSet(superblock_copies).initEmpty();
+    var valid: stdx.BitSetType(superblock_copies) = .{};
     while (valid.count() < Quorums.Threshold.open.count() or prng.boolean()) {
         valid.set(prng.int_inclusive(usize, superblock_copies - 1));
     }
 
     var working_headers: [superblock_copies]SuperBlockHeader = undefined;
     for (&working_headers, 0..) |*header, i| {
-        header.* = if (valid.isSet(i)) headers_valid[i] else header_invalid;
+        header.* = if (valid.is_set(i)) headers_valid[i] else header_invalid;
     }
     prng.shuffle(SuperBlockHeader, &working_headers);
     var repair_headers = working_headers;
@@ -346,7 +346,7 @@ pub fn fuzz_quorum_repairs(
 
     // At the end of all repairs, we expect to have every copy of the superblock.
     // They do not need to be in their home slot.
-    var copies = Quorums.QuorumCount.initEmpty();
+    var copies: Quorums.QuorumCount = .{};
     for (repair_headers) |repair_header| {
         assert(repair_header.checksum == working_quorum.header.checksum);
         copies.set(repair_header.copy);


Reviews:
user: login: batiati
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T10:46:42Z
user: login: matklad
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T11:40:09Z
user: login: matklad
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T11:41:41Z
user: login: batiati
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T12:00:41Z

Review comments:
diff_hunk: @@ -0,0 +1,156 @@
+const stdx = @import("../stdx.zig");
+const std = @import("std");
+const assert = std.debug.assert;
+
+/// Take a u8 to limit to 255 items max (2^8 - 1 = 255).
+/// Use dynamic bitset for larger sizes.
+pub fn BitSetType(comptime with_capacity: u8) type {
+    return struct {
+        // While mathematicaly 0 and 1 are symmetric, we intentionally bias the API to use zeros
+        // default, as zero-initalization requces binary size.
+        bits: Word = 0,
+
+        pub const Word = for (.{ u8, u16, u32, u64, u128, u256 }) |w| {
+            if (@bitSizeOf(w) >= with_capacity) break w;
+        } else unreachable;
+
+        const BitSet = @This();
+
+        pub fn is_set(bit_set: BitSet, index: usize) bool {
+            assert(index < bit_set.capacity());
+            return bit_set.bits & bit(index) != 0;
+        }
+
+        pub fn count(bit_set: BitSet) usize {
+            return @popCount(bit_set.bits);
+        }
+
+        pub inline fn capacity(_: BitSet) usize {
+            return with_capacity;
+        }
+
+        pub fn full(bit_set: BitSet) bool {
+            return bit_set.count() == bit_set.capacity();
+        }
+
+        pub fn empty(bit_set: BitSet) bool {
+            return bit_set.bits == 0;
+        }
+
+        pub fn first_set(bit_set: BitSet) ?usize {
+            if (bit_set.bits == 0) return null;
+            return @ctz(bit_set.bits);
+        }
+
+        pub fn first_unset(bit_set: BitSet) ?usize {
+            const result = @ctz(~bit_set.bits);
+            return if (result < bit_set.capacity()) result else null;
+        }
+
+        pub fn set(bit_set: *BitSet, index: usize) void {
+            assert(index < bit_set.capacity());
+            bit_set.bits |= bit(index);
+        }
+
+        pub fn unset(bit_set: *BitSet, index: usize) void {
+            assert(index < bit_set.capacity());
+            bit_set.bits &= ~bit(index);
+        }
+
+        pub fn set_value(bit_set: *BitSet, index: usize, value: bool) void {
+            if (value) {
+                bit_set.set(index);
+            } else {
+                bit_set.unset(index);
+            }
+        }
+
+        fn bit(index: usize) Word {
+            assert(index < with_capacity);
+            return @as(Word, 1) << @intCast(index);
+        }
+
+        pub fn iterate(bit_set: BitSet) Iterator {
+            return .{ .bits_remain = bit_set.bits };
+        }
+
+        pub const Iterator = struct {
+            bits_remain: Word,
+
+            pub fn next(it: *@This()) ?usize {
+                const result = @ctz(it.bits_remain);
+                if (result >= with_capacity) return null;
+                it.bits_remain &= it.bits_remain - 1;
+                return result;
+            }
+        };
+    };
+}
+
+test BitSetType {
+    var prng = stdx.PRNG.from_seed(92);
+    inline for (.{ 0, 1, 8, 32, 65, 255 }) |N| {
+        const BitSet = BitSetType(N);
+
+        var set: BitSet = .{};
+        var model = try std.DynamicBitSetUnmanaged.initEmpty(std.testing.allocator, N);
+        defer model.deinit(std.testing.allocator);
+
+        for (0..1000) |_| {
+            switch (prng.enum_uniform(std.meta.DeclEnum(BitSet))) {
+                .Word => {
+                    const bit_size =
+                        comptime if (N == 0) 8 else @max(8, try std.math.ceilPowerOfTwo(u16, N));
+                    assert(BitSet.Word == std.meta.Int(.unsigned, bit_size));
+                },
+                .Iterator => {},
+                .is_set => {
+                    if (N > 0) {
+                        const bit = prng.int_inclusive(usize, N - 1);
+                        assert(set.is_set(bit) == model.isSet(bit));
+                    }
+                },
+                .count => assert(set.count() == model.count()),
+                .capacity => assert(set.capacity() == N),
+                .full => assert(set.full() == (model.count() == N)),
+                .empty => assert(set.empty() == (model.count() == 0)),
+                .first_set => assert(set.first_set() == model.findFirstSet()),
+                .first_unset => {
+                    var it = model.iterator(.{ .kind = .unset });
+                    assert(set.first_unset() == it.next());
+                },
+                .set => {
+                    if (N > 0) {
+                        const bit = prng.int_inclusive(usize, N - 1);
+                        set.set(bit);
+                        model.set(bit);
+                    }
+                },
+                .unset => {
+                    if (N > 0) {
+                        const bit = prng.int_inclusive(usize, N - 1);
+                        set.set(bit);
+                        model.set(bit);
path: src/stdx/bit_set.zig
user: login: batiati
body: ```suggestion
                        set.unset(bit);
                        model.unset(bit);
```
created_at: 2025-03-21T10:23:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,156 @@
+const stdx = @import("../stdx.zig");
+const std = @import("std");
+const assert = std.debug.assert;
+
+/// Take a u8 to limit to 255 items max (2^8 - 1 = 255).
+/// Use dynamic bitset for larger sizes.
+pub fn BitSetType(comptime with_capacity: u8) type {
+    return struct {
+        // While mathematicaly 0 and 1 are symmetric, we intentionally bias the API to use zeros
+        // default, as zero-initalization requces binary size.
path: src/stdx/bit_set.zig
user: login: batiati
body: ```suggestion
        // default, as zero-initialization reduces binary size.
```
created_at: 2025-03-21T10:25:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,53 +1,56 @@
 const std = @import("std");
+const stdx = @import("./stdx.zig");
 const assert = std.debug.assert;
 
 /// Take a u8 to limit to 256 items max (2^8 = 256)
 pub fn IOPSType(comptime T: type, comptime size: u8) type {
-    const Map = std.StaticBitSet(size);
+    const Map = stdx.BitSetType(size);
+
     return struct {
         const IOPS = @This();
 
         items: [size]T = undefined,
-        /// 1 bits are free items.
-        free: Map = Map.initFull(),
+        busy: Map = .{},
 
         pub fn acquire(self: *IOPS) ?*T {
-            const i = self.free.findFirstSet() orelse return null;
-            self.free.unset(i);
+            const i = self.busy.first_unset() orelse return null;
+            self.busy.set(i);
             return &self.items[i];
         }
 
         pub fn release(self: *IOPS, item: *T) void {
             item.* = undefined;
             const i = self.index(item);
-            assert(!self.free.isSet(i));
-            self.free.set(i);
+            assert(self.busy.is_set(i));
+            self.busy.unset(i);
         }
 
         pub fn index(self: *IOPS, item: *T) usize {
-            const i = @divExact((@intFromPtr(item) - @intFromPtr(&self.items)), @sizeOf(T));
+            const i = @divExact(
+                (@intFromPtr(item) - @intFromPtr(&self.items)),
+                @sizeOf(T),
+            );
path: src/iops.zig
user: login: batiati
body: Unnecessary line break, still under 100 columns.
created_at: 2025-03-21T10:30:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,53 +1,56 @@
 const std = @import("std");
+const stdx = @import("./stdx.zig");
 const assert = std.debug.assert;
 
 /// Take a u8 to limit to 256 items max (2^8 = 256)
 pub fn IOPSType(comptime T: type, comptime size: u8) type {
-    const Map = std.StaticBitSet(size);
+    const Map = stdx.BitSetType(size);
+
     return struct {
         const IOPS = @This();
 
         items: [size]T = undefined,
-        /// 1 bits are free items.
-        free: Map = Map.initFull(),
+        busy: Map = .{},
path: src/iops.zig
user: login: batiati
body: :tada:  It's much nicer to have it inverted from `free/unset` to `busy/set` !
created_at: 2025-03-21T10:32:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -174,16 +174,17 @@ pub const ClientSessions = struct {
                 assert(header.commit >= session);
 
                 client_sessions.entries_by_client.putAssumeCapacityNoClobber(header.client, i);
-                client_sessions.entries_free.unset(i);
+                client_sessions.entries_present.set(i);
                 client_sessions.entries[i] = .{
                     .session = session,
                     .header = header.*,
                 };
             }
         }
 
-        assert(constants.clients_max - client_sessions.entries_free.count() ==
-            client_sessions.entries_by_client.count());
+        assert(
+            client_sessions.entries_present.count() == client_sessions.entries_by_client.count(),
+        );
path: src/vsr/client_sessions.zig
user: login: batiati
body: :100: 
created_at: 2025-03-21T10:37:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -240,10 +240,10 @@ pub fn JournalType(comptime Replica: type, comptime Storage: type) type {
             constants.journal_iops_write_max
         ][constants.sector_size]u8,
 
-        /// A set bit indicates a chunk of redundant headers that no read has been issued to yet.
-        header_chunks_requested: HeaderChunks = HeaderChunks.initFull(),
+        /// A set bit indicates a chunk of redundant headers that have a read has been issued.
path: src/vsr/journal.zig
user: login: batiati
body: ```suggestion
        /// A set bit indicates a chunk of redundant headers for which a read has been issued.
```
created_at: 2025-03-21T10:39:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,53 +1,56 @@
 const std = @import("std");
+const stdx = @import("./stdx.zig");
 const assert = std.debug.assert;
 
 /// Take a u8 to limit to 256 items max (2^8 = 256)
 pub fn IOPSType(comptime T: type, comptime size: u8) type {
-    const Map = std.StaticBitSet(size);
+    const Map = stdx.BitSetType(size);
+
     return struct {
         const IOPS = @This();
 
         items: [size]T = undefined,
-        /// 1 bits are free items.
-        free: Map = Map.initFull(),
+        busy: Map = .{},
 
         pub fn acquire(self: *IOPS) ?*T {
-            const i = self.free.findFirstSet() orelse return null;
-            self.free.unset(i);
+            const i = self.busy.first_unset() orelse return null;
+            self.busy.set(i);
             return &self.items[i];
         }
 
         pub fn release(self: *IOPS, item: *T) void {
             item.* = undefined;
             const i = self.index(item);
-            assert(!self.free.isSet(i));
-            self.free.set(i);
+            assert(self.busy.is_set(i));
+            self.busy.unset(i);
         }
 
         pub fn index(self: *IOPS, item: *T) usize {
-            const i = @divExact((@intFromPtr(item) - @intFromPtr(&self.items)), @sizeOf(T));
+            const i = @divExact(
+                (@intFromPtr(item) - @intFromPtr(&self.items)),
+                @sizeOf(T),
+            );
path: src/iops.zig
user: login: matklad
body: Yeah, I just find it poorly readable without a line break: it is a fraction, so we might as well make it look like a fraction!
created_at: 2025-03-21T11:40:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,156 @@
+const stdx = @import("../stdx.zig");
+const std = @import("std");
+const assert = std.debug.assert;
+
+/// Take a u8 to limit to 255 items max (2^8 - 1 = 255).
+/// Use dynamic bitset for larger sizes.
+pub fn BitSetType(comptime with_capacity: u8) type {
+    return struct {
+        // While mathematicaly 0 and 1 are symmetric, we intentionally bias the API to use zeros
+        // default, as zero-initalization requces binary size.
+        bits: Word = 0,
+
+        pub const Word = for (.{ u8, u16, u32, u64, u128, u256 }) |w| {
+            if (@bitSizeOf(w) >= with_capacity) break w;
+        } else unreachable;
+
+        const BitSet = @This();
+
+        pub fn is_set(bit_set: BitSet, index: usize) bool {
+            assert(index < bit_set.capacity());
+            return bit_set.bits & bit(index) != 0;
+        }
+
+        pub fn count(bit_set: BitSet) usize {
+            return @popCount(bit_set.bits);
+        }
+
+        pub inline fn capacity(_: BitSet) usize {
+            return with_capacity;
+        }
+
+        pub fn full(bit_set: BitSet) bool {
+            return bit_set.count() == bit_set.capacity();
+        }
+
+        pub fn empty(bit_set: BitSet) bool {
+            return bit_set.bits == 0;
+        }
+
+        pub fn first_set(bit_set: BitSet) ?usize {
+            if (bit_set.bits == 0) return null;
+            return @ctz(bit_set.bits);
+        }
+
+        pub fn first_unset(bit_set: BitSet) ?usize {
+            const result = @ctz(~bit_set.bits);
+            return if (result < bit_set.capacity()) result else null;
+        }
+
+        pub fn set(bit_set: *BitSet, index: usize) void {
+            assert(index < bit_set.capacity());
+            bit_set.bits |= bit(index);
+        }
+
+        pub fn unset(bit_set: *BitSet, index: usize) void {
+            assert(index < bit_set.capacity());
+            bit_set.bits &= ~bit(index);
+        }
+
+        pub fn set_value(bit_set: *BitSet, index: usize, value: bool) void {
+            if (value) {
+                bit_set.set(index);
+            } else {
+                bit_set.unset(index);
+            }
+        }
+
+        fn bit(index: usize) Word {
+            assert(index < with_capacity);
+            return @as(Word, 1) << @intCast(index);
+        }
+
+        pub fn iterate(bit_set: BitSet) Iterator {
+            return .{ .bits_remain = bit_set.bits };
+        }
+
+        pub const Iterator = struct {
+            bits_remain: Word,
+
+            pub fn next(it: *@This()) ?usize {
+                const result = @ctz(it.bits_remain);
+                if (result >= with_capacity) return null;
+                it.bits_remain &= it.bits_remain - 1;
+                return result;
+            }
+        };
+    };
+}
+
+test BitSetType {
+    var prng = stdx.PRNG.from_seed(92);
+    inline for (.{ 0, 1, 8, 32, 65, 255 }) |N| {
+        const BitSet = BitSetType(N);
+
+        var set: BitSet = .{};
+        var model = try std.DynamicBitSetUnmanaged.initEmpty(std.testing.allocator, N);
+        defer model.deinit(std.testing.allocator);
+
+        for (0..1000) |_| {
+            switch (prng.enum_uniform(std.meta.DeclEnum(BitSet))) {
+                .Word => {
+                    const bit_size =
+                        comptime if (N == 0) 8 else @max(8, try std.math.ceilPowerOfTwo(u16, N));
+                    assert(BitSet.Word == std.meta.Int(.unsigned, bit_size));
+                },
+                .Iterator => {},
+                .is_set => {
+                    if (N > 0) {
+                        const bit = prng.int_inclusive(usize, N - 1);
+                        assert(set.is_set(bit) == model.isSet(bit));
+                    }
+                },
+                .count => assert(set.count() == model.count()),
+                .capacity => assert(set.capacity() == N),
+                .full => assert(set.full() == (model.count() == N)),
+                .empty => assert(set.empty() == (model.count() == 0)),
+                .first_set => assert(set.first_set() == model.findFirstSet()),
+                .first_unset => {
+                    var it = model.iterator(.{ .kind = .unset });
+                    assert(set.first_unset() == it.next());
+                },
+                .set => {
+                    if (N > 0) {
+                        const bit = prng.int_inclusive(usize, N - 1);
+                        set.set(bit);
+                        model.set(bit);
+                    }
+                },
+                .unset => {
+                    if (N > 0) {
+                        const bit = prng.int_inclusive(usize, N - 1);
+                        set.set(bit);
+                        model.set(bit);
path: src/stdx/bit_set.zig
user: login: matklad
body: Oops, great catch!
created_at: 2025-03-21T11:41:41Z
author_association: MEMBER

PR comments:
user: login: matklad
created_at: 2025-03-21T12:23:57Z
body: Will merge after release on Monday, I want to get https://github.com/tigerbeetle/tigerbeetle/pull/2834 in first to make sure VOPR is green again.

With me switching polarity in couple of places, I _might_ have messed up set/unset somewhere, and want to get this somewhat more testing
author_association: MEMBER
user: login: batiati
created_at: 2025-03-21T12:36:23Z
body: > Will merge after release on Monday, I want to get #2834 in first to make sure VOPR is green again.
> 
> With me switching polarity in couple of places, I _might_ have messed up set/unset somewhere, and want to get this somewhat more testing

Yeah, lets do it. I have double-checked all places where `.initFull()` was switched to `.{}`, but I also feel like it needs more testing.

I also rebased it to Zig 0.14.0 to get a preview if we would need much refactoring, and it almost compiles without changes.
author_association: CONTRIBUTOR
user: login: matklad
created_at: 2025-03-21T12:40:35Z
body: @batiati given that you are online, want to ✅ #2834? ^^
author_association: MEMBER