Hello r/rust,
I prefer to avoid macros when they can be avoided without major sacrifices, and I ran into this again recently with bitflags.
I wanted several simple bit flags, but every bitflags! macro invocation expanded to about 800 lines of code.
So I wrote bitflags-lite: https://crates.io/crates/bitflags-lite, https://docs.rs/bitflags-lite
The design is simple: instead of generating a distinct struct per flag set, it defines a single generic BitFlags<T, U> where:
* T is storage type like u8 or u32,
* U is a tag that describes which bits are valid, their names, and the default parsing strategy for case sensitivity.
```rust
use bitflags_lite::{BitFlags, BitFlagsSpec, NameMatcher};
struct PermissionsTag;
impl BitFlagsSpec<u32> for PermissionsTag {
const VALID_BITS_MASK: u32 = (1 << 3) - 1;
const FLAG_NAMES: &[&[&str]] = &[&["read"], &["write"], &["exec", "execute"]];
const DEFAULT_NAME_MATCHER: NameMatcher = NameMatcher::AsciiCaseInsensitive;
}
type Permissions = BitFlags<u32, PermissionsTag>;
const READ: Permissions = Permissions::try_from_bit(0).unwrap();
const WRITE: Permissions = Permissions::try_from_bit(1).unwrap();
const EXEC: Permissions = Permissions::try_from_bit(2).unwrap();
const RWX: Permissions = Permissions::try_from_bits(0b111).unwrap();
assert_eq!(RWX, (READ | WRITE | EXEC));
assert!(RWX.contains(READ));
let flags = Permissions::from_str("ReaD | 0x18 | wriTE").unwrap();
assert_eq!(flags, (READ | WRITE | Permissions::from_bits_retain(0x18)));
assert_eq!(flags.to_string(), "read | write | 0x18");
```
That's the whole definition. No hidden codegen.
What you give up:
* No automatic deduction of names and valid bits.
You write VALID_BITS_MASK and FLAG_NAMES by hand and keep them in sync with your constants.
* No named multi-bit flags.
You can still build a set containing several bits (Permissions::try_from_bits(0b111) above),
but there's no way to declare a multi-bit constant inside the spec.
Each bit corresponds to exactly one BitFlag with a name and optional aliases.
What you get instead:
* The generic BitFlags<T, U> type.
* A clearer single-flag (BitFlag) vs. flag-set (BitFlags) distinction.
* A single small trait impl per flag set, rather than a fresh copy of the entire API.
* Alias names for parsing ("exec" and "execute" for the same bit).
* Optional ASCII case-insensitive parsing.
The crate supports no_std and no_alloc, and is const-friendly.
Every constructor and combinator on the concrete integer types is const fn, with generic equivalents in BitFlagsExt / BitFlagExt.
Optional serde, arbitrary, and bytemuck support via features.
I would love to hear your feedback, and I am happy to answer questions!