zig icon indicating copy to clipboard operation
zig copied to clipboard

Improving bitmask/flags

Open Darkfllame opened this issue 1 year ago • 1 comments

Haven't found other issues for this, but as of today, the only way to substitute flags/bitmask in zig are packed structs. They do work but often introduce some impracticallities, such as:

  • bit operations (|, &, etc...), do to these, you must first cast your value to a backing integer, do the op, then re-cast the integer to the packed struct using @as and @bitCast, and that along knowing which bits is which or just doing even more casting. This also include checking if multiple flags are present..
  • creating a bitmask value: you must declare it like a struct, there is no real problem as is, i find it fine as it is, and because you'll probably use bools to do them, but you'll need to type .bitFieldName = true for each of the bits you wanna set, this is really repetitive considering the fact that bitmask fields are often pretty self-explainatory, i.e field names are often like useThing, hasThing or isThing or even just a word like visible, fullscreen or repeat.
  • feel free to find more i guess ? Bitmasks/flags are used in a lot of c APIs (most notably SDL and Vulkan) and can be used in some algorithms (like binary greedy meshing), I also used them in my windowing library, ZWL.

Now a solution I propose is to allow simple enum-like fields when constructing a packed struct value:

const BitMask = packed struct {
  bit1: bool = false,
  bit2: bool = false,
  bit3: bool = false,
};

// like this
const value = BitMask{ .bit1, .bit3};

Bit operations on packed structs:

// same type as above

const a = BitMask{.bit1, .bit3};
const b = a | .{.bit2};
// this wont set the remaining 5 bits
const c = ~b;
const is3rdBit = a.bit3;
// optimizations could include bit-and op
const is2ndAnd3rd = a.bit2 or a.bit3;

Some rules concerning this:

  • any struct field can use the "flag field"s as long as they're typed bool and defaults to false.
  • other required struct fields should be declared if they don't have a default value.
  • the bit operations should only work on packed struct (because they have a backing integer)

This is only if we expand on the syntax, we could also add a "bitmask" type or alike, which would natively include those features along of a backing integer, for example:

const BitMask = bitmask {
  /// these are lsb to hsb, like packed structs
  bit1,
  /// possible documentation
  bit2,
  bit2,
};

Which would introduce a clearer separation for new-comers to the language, while requiring more work to implement a whole new type of data in the compiler.

Darkfllame avatar Oct 10 '24 11:10 Darkfllame

For bit operations that are sensible for a particular type, you can already declare methods on the type.

For your second initial point, for common combinations you may consider decl literals, or again methods (f.e. in the "builder style" of fn setX(Self, x) Self to get const s = (S{}).setX(.x).setY(2) [...] ;). Or maybe you'd prefer a factory method with a field enum fn of([]const FieldEnum) Self and use it as const s = S.of(&.{.visible, .fullscreen, .foo [, ...] });.

I personally don't think that all packed struct types (should) represent bit masks, therefore I think I prefer the explicitness of status-quo. For repeated semantic operations, write a method so callsites don't repeat themselves. I think I wouldn't mind a distinct bitmask construct, but also haven't personally experienced the need for it.

(Tangentially related: https://github.com/ziglang/zig/issues/18882 )

rohlem avatar Oct 10 '24 14:10 rohlem