I needed an internal “typed” bitset. So I created this thing, which is basically a naked version of std_bitset.IntegerBitSet. Max int that can be used = u64.
I “supertyped” it:
CountInt is only used for the popcount
BitInt is exactly the right size.
Any comments? (except my braces on new line, that’s personal)
Too bad I have to truncate tthe result of @ctz
pub fn BitSet(comptime size: u7) type
{
comptime if (size == 0) @compileError("BitSet size 0 not supported");
comptime if (size > 64) @compileError("BitSet size > 64 not supported");
return packed struct
{
const Self = @This();
pub const zeroes: Self = .{};
/// The integer type used to represent a mask in this bit set. For BitSet(64) this will be a u64.
pub const MaskInt = std.meta.Int(.unsigned, size);
/// The integer type used to represent a bit number in this bit set. For BitSet(u64) this will be a u6.
pub const BitInt = std.math.Log2Int(MaskInt);
const one_bigger = std.meta.Int(.unsigned, size + 1);
/// The integer type used to represent counting of bits. For BitSet(64) this will be a u7.
pub const CountInt = std.math.Log2Int(one_bigger);
// /// The bit mask, as a single integer
mask: MaskInt = 0,
pub fn init(mask: MaskInt) Self
{
return .{ .mask = mask };
}
pub fn is_set(self: Self, bit: BitInt) bool
{
return (self.mask & mask_bit(bit)) != 0;
}
pub fn set(self: *Self, bit: BitInt) void
{
self.mask |= mask_bit(bit);
}
pub fn unset(self: *Self, bit: BitInt) void
{
self.mask &= ~mask_bit(bit);
}
fn mask_bit(bit: BitInt) MaskInt
{
return @as(MaskInt, 1) << @as(BitInt, @intCast(bit));
}
pub fn count(self: Self) CountInt
{
return @popCount(self.mask);
}
pub fn iterator(self: Self) Iterator()
{
return .{ .bits_remaining = self.mask };
}
fn Iterator() type
{
return struct
{
const IterSelf = @This();
/// all bits which have not yet been iterated over
bits_remaining: MaskInt,
/// Returns tee next set bit.
pub fn next(self: *IterSelf) ?BitInt
{
if (self.bits_remaining == 0) return null;
const next_index = @ctz(self.bits_remaining);
self.bits_remaining &= (self.bits_remaining - 1);
return @truncate(next_index);
}
};
}
};
}
Now I run into troubles when replacing a “raw” bitmask (u15) inside a packed struct with this BitSet(15).
When calling a function the compiler complains about alignment.
Is there a way to avoid having to do a @ptrCast(@alignCast(&self.bitset))
It is not possible to do any aligning or ordering in packed structs… It is done by the compiler.
Minimal example. In the 2nd method I am doing something terribly wrong also…
I think the easiest two options would be to either flatten the PackedStruct so that it contains only primitive types (integers and booleans) instead of nested packed structs that also need to be addressable.
Or make the BitSet 16 bits wide for easier alignment.
With the 15 Bits and nesting I don’t know how to get it aligned properly, with the things I tried.
I think you will either have to give up on those or give up the nesting and instead write it more flat.
Not entirely sure, because I haven’t actually tried this sort of nesting with packed structs that aren’t a multiple of 8 bits, except just now. Maybe there is some way I am not aware of.
Yeah it is a complicated case. I need all kinds of sizes in the packed struct and also need it to be small. I only have 6 bits spare.
So I will stick to the raw u15.
This makes the calling code a little less ergonomic with self.bits = self.bits.set(pos); instead of just `self.bits.set(pos);. Full Example on Godbolt.
Add variants of your packed structs with the usual (not packed) memory layout. These variants would have all the functions you want. Add additional conversions between the packed and non-packed variants. Your code would change to a “load from packed - work with the not packed variant - store back to packed”-style.