From 18e9855f52549683b1309982538d2c3da443014b Mon Sep 17 00:00:00 2001 From: winstonallo Date: Thu, 25 Sep 2025 18:48:55 +0200 Subject: [PATCH 01/19] Allow types from u1 to u128 in bitmap and update tests --- macros/src/generator.rs | 3 ++- macros/src/parser.rs | 20 +++++++++++--------- tests/trybuild_tests.rs | 6 ++++++ tests/ui/bitmap_too_large.rs | 13 ++----------- tests/ui/invalid_type.rs | 2 +- tests/ui/invalid_type.stderr | 6 +++--- tests/ui/invalid_type_zero.rs | 9 +++++++++ tests/ui/invalid_type_zero.stderr | 5 +++++ 8 files changed, 39 insertions(+), 25 deletions(-) create mode 100644 tests/ui/invalid_type_zero.rs create mode 100644 tests/ui/invalid_type_zero.stderr diff --git a/macros/src/generator.rs b/macros/src/generator.rs index 575cfe0..4a0f0c2 100644 --- a/macros/src/generator.rs +++ b/macros/src/generator.rs @@ -8,7 +8,7 @@ pub fn expand_bitmap(input: BitmapInput) -> syn::Result { let fields = &input.fields; let size = input.fields.iter().map(|f| f.size).sum(); - if size > 64 { + if size > 128 { return Err(syn::Error::new_spanned(name, "Too many fields: max supported size is 64 bits")); } @@ -17,6 +17,7 @@ pub fn expand_bitmap(input: BitmapInput) -> syn::Result { 9..=16 => quote! { u16 }, 17..=32 => quote! { u32 }, 33..=64 => quote! { u64 }, + 65..=128 => quote! { u128 }, _ => unreachable!(), }; diff --git a/macros/src/parser.rs b/macros/src/parser.rs index d78aa7e..a7f1ca0 100644 --- a/macros/src/parser.rs +++ b/macros/src/parser.rs @@ -33,16 +33,18 @@ impl Parse for FieldDef { let _: Token![:] = input.parse()?; let ty: Ident = input.parse()?; - let size = match ty.to_string().as_str() { - "u1" => 1, - "u2" => 2, - "u3" => 3, - "u4" => 4, - "u5" => 5, - "u6" => 6, - "u7" => 7, - _ => return Err(syn::Error::new_spanned(ty, "Expected one of u1, u2, u3, u4, u5, u6, and u7")), + let ty_str = ty.to_string(); + let ty_str = ty_str.as_str(); + if !ty_str.starts_with("u") { + return Err(syn::Error::new_spanned(ty, format!("Invalid type {ty_str}, expected u{{1..128}}"))); + } + let size = *match &ty_str[1..].parse::() { + Ok(val) => val, + Err(e) => return Err(syn::Error::new_spanned(ty, format!("Could not parse type size: {e}"))), }; + if size == 0 || size > 128 { + return Err(syn::Error::new_spanned(ty, format!("Invalid size for {ty_str}, expected u{{1..128}}"))); + } Ok(FieldDef { name, size }) } diff --git a/tests/trybuild_tests.rs b/tests/trybuild_tests.rs index cf42777..ec43226 100644 --- a/tests/trybuild_tests.rs +++ b/tests/trybuild_tests.rs @@ -9,3 +9,9 @@ fn invalid_type() { let t = trybuild::TestCases::new(); t.compile_fail("tests/ui/invalid_type.rs"); } + +#[test] +fn invalid_type_zero() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/ui/invalid_type_zero.rs"); +} diff --git a/tests/ui/bitmap_too_large.rs b/tests/ui/bitmap_too_large.rs index 8ac475a..0b6d340 100644 --- a/tests/ui/bitmap_too_large.rs +++ b/tests/ui/bitmap_too_large.rs @@ -3,17 +3,8 @@ use macros::bitmap; fn main() { bitmap!( struct Bits { - field0: u1, - field1: u7, - field2: u7, - field3: u7, - field4: u7, - field5: u7, - field6: u7, - field7: u7, - field8: u7, - field9: u7, - field10: u7, + field0: u128, + field1: u1, } ); } diff --git a/tests/ui/invalid_type.rs b/tests/ui/invalid_type.rs index e0f66a0..699814f 100644 --- a/tests/ui/invalid_type.rs +++ b/tests/ui/invalid_type.rs @@ -3,7 +3,7 @@ use macros::bitmap; fn main() { bitmap!( struct Bits { - field0: u8, + field0: u129, } ); } diff --git a/tests/ui/invalid_type.stderr b/tests/ui/invalid_type.stderr index 93f677f..3b2eefb 100644 --- a/tests/ui/invalid_type.stderr +++ b/tests/ui/invalid_type.stderr @@ -1,5 +1,5 @@ -error: Expected one of u1, u2, u3, u4, u5, u6, and u7 +error: Invalid size for u129, expected u{1..128} --> tests/ui/invalid_type.rs:6:21 | -6 | field0: u8, - | ^^ +6 | field0: u129, + | ^^^^ diff --git a/tests/ui/invalid_type_zero.rs b/tests/ui/invalid_type_zero.rs new file mode 100644 index 0000000..c1b38e0 --- /dev/null +++ b/tests/ui/invalid_type_zero.rs @@ -0,0 +1,9 @@ +use macros::bitmap; + +fn main() { + bitmap!( + struct Bits { + field0: u0, + } + ); +} diff --git a/tests/ui/invalid_type_zero.stderr b/tests/ui/invalid_type_zero.stderr new file mode 100644 index 0000000..44a7362 --- /dev/null +++ b/tests/ui/invalid_type_zero.stderr @@ -0,0 +1,5 @@ +error: Invalid size for u0, expected u{1..128} + --> tests/ui/invalid_type_zero.rs:6:21 + | +6 | field0: u0, + | ^^ From 8821c88bbcf0125757f0119b4523e68a7be59789 Mon Sep 17 00:00:00 2001 From: winstonallo Date: Thu, 25 Sep 2025 19:57:01 +0200 Subject: [PATCH 02/19] Add get_packed_layout function --- macros/src/generator.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/macros/src/generator.rs b/macros/src/generator.rs index 4a0f0c2..eb28618 100644 --- a/macros/src/generator.rs +++ b/macros/src/generator.rs @@ -3,10 +3,24 @@ use quote::quote; use crate::parser::BitmapInput; +fn get_packed_layout(size: usize) -> Vec { + let mut running_size = size; + let mut sizes = Vec::::new(); + let usizes = &[128, 64, 32, 16, 8]; + + for usz in usizes { + while running_size >= *usz as usize { + sizes.push(*usz); + running_size -= *usz as usize; + } + } + sizes +} + pub fn expand_bitmap(input: BitmapInput) -> syn::Result { let name = &input.name; let fields = &input.fields; - let size = input.fields.iter().map(|f| f.size).sum(); + let size: usize = input.fields.iter().map(|f| f.size as usize).sum(); if size > 128 { return Err(syn::Error::new_spanned(name, "Too many fields: max supported size is 64 bits")); From 27cb17b4b0cb9a94477fa225a3e62c2c59fffe4b Mon Sep 17 00:00:00 2001 From: winstonallo Date: Thu, 25 Sep 2025 20:15:16 +0200 Subject: [PATCH 03/19] Fix edge case with leftover size --- macros/src/generator.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/macros/src/generator.rs b/macros/src/generator.rs index eb28618..11c0e7f 100644 --- a/macros/src/generator.rs +++ b/macros/src/generator.rs @@ -4,16 +4,21 @@ use quote::quote; use crate::parser::BitmapInput; fn get_packed_layout(size: usize) -> Vec { + let usizes = [128, 64, 32, 16, 8]; let mut running_size = size; let mut sizes = Vec::::new(); - let usizes = &[128, 64, 32, 16, 8]; - for usz in usizes { - while running_size >= *usz as usize { - sizes.push(*usz); - running_size -= *usz as usize; + for &usz in &usizes { + while running_size >= usz as usize { + sizes.push(usz); + running_size -= usz as usize; } } + + if running_size > 0 { + sizes.push(8); + } + sizes } From 525c4c709b45b16bda4c1409cce69823c662ebcc Mon Sep 17 00:00:00 2001 From: winstonallo Date: Thu, 25 Sep 2025 20:16:35 +0200 Subject: [PATCH 04/19] Call get_packed_layout to avoid clippy error --- macros/src/generator.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/macros/src/generator.rs b/macros/src/generator.rs index 11c0e7f..5f27423 100644 --- a/macros/src/generator.rs +++ b/macros/src/generator.rs @@ -26,6 +26,7 @@ pub fn expand_bitmap(input: BitmapInput) -> syn::Result { let name = &input.name; let fields = &input.fields; let size: usize = input.fields.iter().map(|f| f.size as usize).sum(); + let _packed_layout = get_packed_layout(size); if size > 128 { return Err(syn::Error::new_spanned(name, "Too many fields: max supported size is 64 bits")); From d3ac3deb378c17cc55cfd2c7576a25a68d87333e Mon Sep 17 00:00:00 2001 From: winstonallo Date: Sat, 27 Sep 2025 18:12:49 +0200 Subject: [PATCH 05/19] Rename running_size to remainder in get_packed_layout --- macros/src/generator.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/macros/src/generator.rs b/macros/src/generator.rs index 5f27423..fe7283b 100644 --- a/macros/src/generator.rs +++ b/macros/src/generator.rs @@ -5,17 +5,17 @@ use crate::parser::BitmapInput; fn get_packed_layout(size: usize) -> Vec { let usizes = [128, 64, 32, 16, 8]; - let mut running_size = size; + let mut remainder = size; let mut sizes = Vec::::new(); for &usz in &usizes { - while running_size >= usz as usize { + while remainder >= usz as usize { sizes.push(usz); - running_size -= usz as usize; + remainder -= usz as usize; } } - if running_size > 0 { + if remainder > 0 { sizes.push(8); } From 5f2aa5c031ef4518d0d7e307bf9b4da09e56c349 Mon Sep 17 00:00:00 2001 From: winstonallo Date: Sat, 27 Sep 2025 18:26:34 +0200 Subject: [PATCH 06/19] Add condition checking whether a field spans multiple storage units --- macros/src/generator.rs | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/macros/src/generator.rs b/macros/src/generator.rs index fe7283b..71a8b0e 100644 --- a/macros/src/generator.rs +++ b/macros/src/generator.rs @@ -22,27 +22,32 @@ fn get_packed_layout(size: usize) -> Vec { sizes } -pub fn expand_bitmap(input: BitmapInput) -> syn::Result { - let name = &input.name; - let fields = &input.fields; - let size: usize = input.fields.iter().map(|f| f.size as usize).sum(); - let _packed_layout = get_packed_layout(size); - - if size > 128 { - return Err(syn::Error::new_spanned(name, "Too many fields: max supported size is 64 bits")); - } - - let storage_ty = match size { +fn get_storage_ty(size: u8) -> TokenStream2 { + match size { 0..=8 => quote! { u8 }, 9..=16 => quote! { u16 }, 17..=32 => quote! { u32 }, 33..=64 => quote! { u64 }, 65..=128 => quote! { u128 }, _ => unreachable!(), - }; + } +} + +pub fn expand_bitmap(input: BitmapInput) -> syn::Result { + let name = &input.name; + let fields = &input.fields; + let size: usize = input.fields.iter().map(|f| f.size as usize).sum(); + let _packed_layout = get_packed_layout(size); + + let storage_ty = get_storage_ty(size as u8); let mut bit_index = 0; + let mut storage_index = 0; + let mut current_storage_ty_index = 0; let accessors = fields.iter().map(|ident| { + if ident.size + current_storage_ty_index >= _packed_layout[storage_index] { + // handle field spanning multiple storage units + } let index: u8 = bit_index; bit_index += ident.size; let setter_name = Ident::new(&format!("set_{}", ident.name), ident.name.span()); From eb96a867933feb7d0387d2aa2e140ee48914ca72 Mon Sep 17 00:00:00 2001 From: winstonallo Date: Sun, 28 Sep 2025 13:28:38 +0200 Subject: [PATCH 07/19] Add doc-tests for bitmap! to tests.yml --- .github/workflows/tests.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d0bf145..0e7fd2d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,5 +20,8 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Run tests + - name: Run crate tests run: cargo test + + - name: Run macro doc-tests + run: cargo test --manifest-path macros/Cargo.toml From 45f510e7b3e69f2e0a04b7b8959ceabe09d64fd2 Mon Sep 17 00:00:00 2001 From: winstonallo Date: Sun, 28 Sep 2025 13:54:28 +0200 Subject: [PATCH 08/19] Add back maximum size of 128 bits --- macros/src/generator.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/macros/src/generator.rs b/macros/src/generator.rs index 71a8b0e..00186e6 100644 --- a/macros/src/generator.rs +++ b/macros/src/generator.rs @@ -39,6 +39,10 @@ pub fn expand_bitmap(input: BitmapInput) -> syn::Result { let size: usize = input.fields.iter().map(|f| f.size as usize).sum(); let _packed_layout = get_packed_layout(size); + if size > 128 { + return Err(syn::Error::new_spanned(name, "Too many fields: maximum supported size is 128 bits")); + } + let storage_ty = get_storage_ty(size as u8); let mut bit_index = 0; From 16ce53830b9b27b0ee2150afb5c253f6cba921f5 Mon Sep 17 00:00:00 2001 From: winstonallo Date: Sun, 28 Sep 2025 14:49:30 +0200 Subject: [PATCH 09/19] Update documentation for `bitmap!` --- macros/src/lib.rs | 99 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 86 insertions(+), 13 deletions(-) diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 8714d7b..edb4f41 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -4,24 +4,96 @@ use syn::parse_macro_input; mod generator; mod parser; -/// Generates a bitmap struct from the given definition. +/// Generates a packed bitmap newtype struct with field-level bit access. /// -/// The macro expands to a newtype struct around a `u8` to `u64` (depending on the total size -/// of the definition), with automatically generated getters and setters for each field. +/// The macro expands to a newtype struct around a `u8` to `u128`, depending on the total bit width +/// of the definition, with automatically generated getters and setters for each field. /// -/// ### Supported field types -/// - `u1` through `u7` +/// ### Supported field types: +/// ``` +/// use macros::bitmap; +/// +/// bitmap!( +/// struct Bits { +/// flag: u1, +/// counter: u7, +/// } +/// ); +/// ``` +/// Each field must be in the form `uN`, where `1 <= N <= 128`. +/// ### Maximum total size +/// `bitmap!` uses the smallest possible integer type such that `total_bit_width <= integer.bit_width`. +/// The total bit width must fit into a `u128`. If you need more than that, consider using a `Vec` +/// of `bitmap`s. +/// ### Storage order +/// Fields are packed from **most significant bit (MSB)** to **least significant bit (LSB)**, matching +/// big-endian order. +/// +/// This means the first declared field is stored in the highest bits of the underlying storage integer. +/// ``` +/// use macros::bitmap; +/// +/// bitmap!( +/// struct Bits { +/// a: u8, +/// b: u8, +/// } +/// ); +/// +/// let mut bits = Bits(0); +/// bits.set_a(0xaa) +/// .set_b(0xbb); +/// +/// assert_eq!(*bits, 0xaabb); +/// ``` +/// +/// ### Note +/// `bitmap!` is built with hardware configuration in mind, where most packed bitmaps have a size +/// aligned to integer sizes. It does not use the _smallest possible size_: a bitmap with only one `u33` +/// field will take up 64 bits of space. +/// ``` +/// use macros::bitmap; +/// +/// bitmap!( +/// struct Bits { +/// field: u33, +/// } +/// ); +/// +/// assert_eq!(core::mem::size_of::(), 8); +/// ``` +/// +/// ### API +/// #### Accessing fields +/// For each field `name: T`, where `T` is the smallest possible integer such that +/// `field_size <= integer.size`, `bitmap!` generates: /// -/// ### Current Limitations -/// - Total bit size must be ≤ 64 bits. -/// - No values larger than `u7` currently supported +/// - `fn name(&self) -> T` — returns the value for `name` +/// - `fn set_name(&mut self, val: T)` — sets the value for `name` /// -/// ### Generated API -/// For each field `name: T`, the macro generates: -/// - `fn name(&self) -> T` — returns the field value. -/// - `fn set_name(&mut self, val: T)` — sets the field value. +/// #### Accessing the raw value +/// For the struct `Bits(T)`, where `T` is the unsigned integer type used for storage, +/// the following traits are implemented: +/// - `From for T` +/// - `Deref for Bits`, with `fn deref(&self) -> T` +/// +/// ``` +/// use macros::bitmap; +/// +/// bitmap!( +/// struct Bits { +/// a: u32, +/// b: u16, +/// c: u16, +/// } +/// ); +/// +/// let bits = Bits(0); +/// let underlying_u64: u64 = bits.into(); +/// let underlying_u64 = *bits; +/// ``` /// -/// ### Example +/// ### Usage Example /// ``` /// use macros::bitmap; /// @@ -43,6 +115,7 @@ mod parser; /// assert_eq!(player.imposter(), 1); /// assert_eq!(player.finished_tasks(), 5); /// assert_eq!(player.kills(), 3); +/// assert_eq!(*player, 0b01101011); /// ``` #[proc_macro] pub fn bitmap(input: TokenStream) -> TokenStream { From 5b378bda99f64ea58447985e34161ee9971b5956 Mon Sep 17 00:00:00 2001 From: winstonallo Date: Sun, 28 Sep 2025 14:49:40 +0200 Subject: [PATCH 10/19] Remove crate-level docs --- src/lib.rs | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d796bfd..9bc3175 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,26 +1,3 @@ -//! Generates a bitmap from the passed struct definition. The generated -//! code includes a struct with getters and setters for each field. -//! Supported types: `u1`, `u2`, `u3`, `u4`, `u5`, `u6`, and `u7`, maximum -//! size: 64 bits. -//! -//! # Example: -//! ``` -//! use macros::bitmap; -//! -//! bitmap!( -//! struct Player { -//! imposter: u1, -//! finished_tasks: u3, -//! kills: u3, -//! } -//! ); -//! -//! let mut player = Player(0); -//! assert_eq!(std::mem::size_of::(), 1); -//! player.set_imposter(1); -//! player.set_finished_tasks(5); -//! player.set_kills(3); -//! ``` pub use macros::bitmap; #[test] From b1a638921ff5e82b1d3563f396a2be8a940d8ea4 Mon Sep 17 00:00:00 2001 From: winstonallo Date: Sun, 28 Sep 2025 14:50:25 +0200 Subject: [PATCH 11/19] Calculate bit_index backwards to store fields in big-endian --- macros/src/generator.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/macros/src/generator.rs b/macros/src/generator.rs index 00186e6..499d6c8 100644 --- a/macros/src/generator.rs +++ b/macros/src/generator.rs @@ -45,15 +45,10 @@ pub fn expand_bitmap(input: BitmapInput) -> syn::Result { let storage_ty = get_storage_ty(size as u8); - let mut bit_index = 0; - let mut storage_index = 0; - let mut current_storage_ty_index = 0; + let mut bit_index = size; let accessors = fields.iter().map(|ident| { - if ident.size + current_storage_ty_index >= _packed_layout[storage_index] { - // handle field spanning multiple storage units - } - let index: u8 = bit_index; - bit_index += ident.size; + bit_index -= ident.size as usize; + let index: usize = bit_index; let setter_name = Ident::new(&format!("set_{}", ident.name), ident.name.span()); let name = ident.name.to_owned(); let size = ident.size; From e2d3b83f042f82d84832d478c167975cd829b943 Mon Sep 17 00:00:00 2001 From: winstonallo Date: Sun, 28 Sep 2025 14:51:03 +0200 Subject: [PATCH 12/19] Add explicit casts/type annotations to ensure smallest possible type in getters/setters --- macros/src/generator.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/macros/src/generator.rs b/macros/src/generator.rs index 499d6c8..2c9edb8 100644 --- a/macros/src/generator.rs +++ b/macros/src/generator.rs @@ -54,11 +54,13 @@ pub fn expand_bitmap(input: BitmapInput) -> syn::Result { let size = ident.size; let mask = quote! { ((0b1 << #size) - 1) as #storage_ty }; quote! { - pub fn #name(&self) -> #storage_ty { - (self.0 >> #index) & #mask + #[inline] + pub const fn #name(&self) -> #this_storage_ty { + ((self.0 >> #index) & #mask) as #this_storage_ty } - pub fn #setter_name(&mut self, val: u8) -> &mut Self { + #[inline] + pub fn #setter_name(&mut self, val: #this_storage_ty) -> &mut Self { self.0 = ((self.0 & !((#mask) << #index)) | (((val as #storage_ty) & #mask) << #index)); self } From b1f4afba2d912fae65358b4b8a715ba14bcc17c6 Mon Sep 17 00:00:00 2001 From: winstonallo Date: Sun, 28 Sep 2025 14:52:12 +0200 Subject: [PATCH 13/19] Store mask in storage_ty for size+1 and hardcode u128::MAX to prevent overflows --- macros/src/generator.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/macros/src/generator.rs b/macros/src/generator.rs index 2c9edb8..b353bf5 100644 --- a/macros/src/generator.rs +++ b/macros/src/generator.rs @@ -52,7 +52,13 @@ pub fn expand_bitmap(input: BitmapInput) -> syn::Result { let setter_name = Ident::new(&format!("set_{}", ident.name), ident.name.span()); let name = ident.name.to_owned(); let size = ident.size; - let mask = quote! { ((0b1 << #size) - 1) as #storage_ty }; + let this_storage_ty = get_storage_ty(size); + let mask = if size != 128 { + let mask_ty = get_storage_ty(size + 1); + quote! { (((0b1 as #mask_ty) << #size) - 1) as #storage_ty } + } else { + quote! { 340282366920938463463374607431768211455 } + }; quote! { #[inline] pub const fn #name(&self) -> #this_storage_ty { From cb75dfde9beff9ce89c6432913f5825e8817cb2d Mon Sep 17 00:00:00 2001 From: winstonallo Date: Sun, 28 Sep 2025 14:52:29 +0200 Subject: [PATCH 14/19] Remove pub from struct definition --- macros/src/generator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macros/src/generator.rs b/macros/src/generator.rs index b353bf5..9ca517f 100644 --- a/macros/src/generator.rs +++ b/macros/src/generator.rs @@ -76,7 +76,7 @@ pub fn expand_bitmap(input: BitmapInput) -> syn::Result { Ok(quote! { #[derive(Debug, Clone, Copy)] #[repr(transparent)] - pub struct #name(pub #storage_ty); + pub struct #name(#storage_ty); impl #name { #(#accessors)* From 5e7ebaf72cc0d5d1372a42c94d49059025559af6 Mon Sep 17 00:00:00 2001 From: winstonallo Date: Sun, 28 Sep 2025 14:52:51 +0200 Subject: [PATCH 15/19] Implement From<#name> for #storage_ty and Deref for #name in macro --- macros/src/generator.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/macros/src/generator.rs b/macros/src/generator.rs index 9ca517f..98692c9 100644 --- a/macros/src/generator.rs +++ b/macros/src/generator.rs @@ -81,5 +81,18 @@ pub fn expand_bitmap(input: BitmapInput) -> syn::Result { impl #name { #(#accessors)* } + + impl From<#name> for #storage_ty { + fn from(value: #name) -> Self { + value.0 + } + } + + impl core::ops::Deref for #name { + type Target = #storage_ty; + fn deref(&self) -> &Self::Target { + &self.0 + } + } }) } From 9af2a539680beee548fd84b3d70fc1475a1900c5 Mon Sep 17 00:00:00 2001 From: winstonallo Date: Sun, 28 Sep 2025 14:53:01 +0200 Subject: [PATCH 16/19] Update tests --- src/lib.rs | 66 +++++++++++++++++++++++++++----- tests/ui/bitmap_too_large.stderr | 2 +- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9bc3175..29f2605 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,11 +23,11 @@ fn two_bits() { let mut bits = Bits(0b10); bits.set_a(0b1); bits.set_b(0b0); - assert_eq!(bits.0, 0b01); + assert_eq!(*bits, 0b10); } #[test] -fn sixty_four_bits() { +fn sixty_four_bits_funky_layout() { bitmap!( struct Bits { a: u1, @@ -44,7 +44,59 @@ fn sixty_four_bits() { ); let mut bits = Bits(0xFF00FF00FF00FF00); bits.set_j(0b0000000).set_i(0b1111111).set_a(0b1); - assert_eq!(bits.0, 0x01FCFF00FF00FF01); + assert_eq!(*bits, 0xFF00FF00FF00FF80); +} + +#[test] +fn sixty_four_bits_aligned() { + bitmap!( + struct Bits { + a: u32, + b: u32, + } + ); + let mut bits = Bits(0xFF00FF00FF00FF00); + bits.set_a(0xFFFFFFFF).set_b(0b00000000); + assert_eq!(*bits, 0xFFFFFFFF00000000); +} + +#[test] +fn hundred_and_twenty_eight_bits_funky_layout() { + bitmap!( + struct Bits { + a: u40, + b: u25, + c: u31, + d: u16, + e: u9, + f: u7, + } + ); + + let mut bits = Bits(0xFF00FF00FF00FF00FF00FF00FF00FF00); + bits.set_a(0xAAAAAAAAAA) + .set_b(0b1111111111111111111111111) + .set_c(0b0000000000000000000000000000000) + .set_d(0x6666) + .set_e(0b111111111) + .set_f(0b0000000); + + assert_eq!(*bits, 0xaaaaaaaaaaffffff800000006666ff80); +} + +#[test] +fn hundred_and_twenty_eight_bits_aligned() { + bitmap!( + struct Bits { + a: u32, + b: u32, + c: u32, + d: u32, + } + ); + let mut bits = Bits(0xFF00FF00FF00FF00FF00FF00FF00FF00); + bits.set_a(0xFFFFFFFF).set_b(0x00000000).set_c(0x42424242).set_d(0x66666666); + assert_eq!(*bits, 0xFFFFFFFF000000004242424266666666); } macro_rules! test_width { @@ -63,10 +115,4 @@ macro_rules! test_width { }; } -test_width!(u1, 1); -test_width!(u2, 3); -test_width!(u3, 7); -test_width!(u4, 15); -test_width!(u5, 31); -test_width!(u6, 63); -test_width!(u7, 127); +include!(concat!(env!("OUT_DIR"), "/generated_tests.rs")); diff --git a/tests/ui/bitmap_too_large.stderr b/tests/ui/bitmap_too_large.stderr index 7294011..fbd5ba4 100644 --- a/tests/ui/bitmap_too_large.stderr +++ b/tests/ui/bitmap_too_large.stderr @@ -1,4 +1,4 @@ -error: Too many fields: max supported size is 64 bits +error: Too many fields: maximum supported size is 128 bits --> tests/ui/bitmap_too_large.rs:5:16 | 5 | struct Bits { From 7efc531e7b26529e99ff858babb0350d25fa4c37 Mon Sep 17 00:00:00 2001 From: winstonallo Date: Sun, 28 Sep 2025 14:53:27 +0200 Subject: [PATCH 17/19] Generate bit_width tests for all possible types in build.rs --- build.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 build.rs diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..0f87bc0 --- /dev/null +++ b/build.rs @@ -0,0 +1,16 @@ +use std::{fs::File, io::Write, path::Path}; + +fn main() { + let out_dir = std::env::var("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("generated_tests.rs"); + let mut f = File::create(&dest_path).unwrap(); + + for n in 1..=128 { + let max_val = if n != 128 { + (1u128 << n) - 1 + } else { + 340282366920938463463374607431768211455 + }; + writeln!(f, "test_width!(u{n}, {max_val});").unwrap(); + } +} From 27accc26541a570ffe762303fa65f4c865e803ec Mon Sep 17 00:00:00 2001 From: winstonallo Date: Sun, 28 Sep 2025 14:58:01 +0200 Subject: [PATCH 18/19] Restructure docs to have the usage example at the top --- macros/src/lib.rs | 107 +++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 54 deletions(-) diff --git a/macros/src/lib.rs b/macros/src/lib.rs index edb4f41..6f475b0 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -9,6 +9,59 @@ mod parser; /// The macro expands to a newtype struct around a `u8` to `u128`, depending on the total bit width /// of the definition, with automatically generated getters and setters for each field. /// +/// ### API +/// #### Usage Example +/// ``` +/// use macros::bitmap; +/// +/// bitmap!( +/// struct Player { +/// imposter: u1, +/// finished_tasks: u3, +/// kills: u3, +/// } +/// ); +/// +/// let mut player = Player(0); +/// assert_eq!(std::mem::size_of::(), 1); +/// +/// player.set_imposter(1); +/// player.set_finished_tasks(5); +/// player.set_kills(3); +/// +/// assert_eq!(player.imposter(), 1); +/// assert_eq!(player.finished_tasks(), 5); +/// assert_eq!(player.kills(), 3); +/// assert_eq!(*player, 0b01101011); +/// ``` +/// #### Accessing fields +/// For each field `name: T`, where `T` is the smallest possible integer such that +/// `field_size <= integer.size`, `bitmap!` generates: +/// +/// - `fn name(&self) -> T` — returns the value for `name` +/// - `fn set_name(&mut self, val: T)` — sets the value for `name` +/// +/// #### Accessing the raw value +/// For the struct `Bits(T)`, where `T` is the unsigned integer type used for storage, +/// the following traits are implemented: +/// - `From for T` +/// - `Deref for Bits`, with `fn deref(&self) -> T` +/// +/// ``` +/// use macros::bitmap; +/// +/// bitmap!( +/// struct Bits { +/// a: u32, +/// b: u16, +/// c: u16, +/// } +/// ); +/// +/// let bits = Bits(0); +/// let underlying_u64: u64 = bits.into(); +/// let underlying_u64 = *bits; +/// ``` /// ### Supported field types: /// ``` /// use macros::bitmap; @@ -63,60 +116,6 @@ mod parser; /// assert_eq!(core::mem::size_of::(), 8); /// ``` /// -/// ### API -/// #### Accessing fields -/// For each field `name: T`, where `T` is the smallest possible integer such that -/// `field_size <= integer.size`, `bitmap!` generates: -/// -/// - `fn name(&self) -> T` — returns the value for `name` -/// - `fn set_name(&mut self, val: T)` — sets the value for `name` -/// -/// #### Accessing the raw value -/// For the struct `Bits(T)`, where `T` is the unsigned integer type used for storage, -/// the following traits are implemented: -/// - `From for T` -/// - `Deref for Bits`, with `fn deref(&self) -> T` -/// -/// ``` -/// use macros::bitmap; -/// -/// bitmap!( -/// struct Bits { -/// a: u32, -/// b: u16, -/// c: u16, -/// } -/// ); -/// -/// let bits = Bits(0); -/// let underlying_u64: u64 = bits.into(); -/// let underlying_u64 = *bits; -/// ``` -/// -/// ### Usage Example -/// ``` -/// use macros::bitmap; -/// -/// bitmap!( -/// struct Player { -/// imposter: u1, -/// finished_tasks: u3, -/// kills: u3, -/// } -/// ); -/// -/// let mut player = Player(0); -/// assert_eq!(std::mem::size_of::(), 1); -/// -/// player.set_imposter(1); -/// player.set_finished_tasks(5); -/// player.set_kills(3); -/// -/// assert_eq!(player.imposter(), 1); -/// assert_eq!(player.finished_tasks(), 5); -/// assert_eq!(player.kills(), 3); -/// assert_eq!(*player, 0b01101011); -/// ``` #[proc_macro] pub fn bitmap(input: TokenStream) -> TokenStream { let parsed = parse_macro_input!(input as parser::BitmapInput); From a89f7af9334f6844909d1cad0e981db57bea44e4 Mon Sep 17 00:00:00 2001 From: winstonallo Date: Sun, 28 Sep 2025 15:00:23 +0200 Subject: [PATCH 19/19] Update README.md --- README.md | 125 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 110 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index ccf304e..6d9ec18 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ -# bitmap +# `bitmap!` -Procedural macro for defining bitmap structs packed as tightly as possible. +Generates a packed bitmap newtype struct with field-level bit access. -- Define fields with `u1` to `u7` widths -- Up to 64 bits total per struct (64 `u1`s, 9 `u7`s + 1 `u1`, etc.) -- Getters and setters are generated +The macro expands to a newtype struct around a `u8` to `u128`, depending on the total bit width +of the definition, with automatically generated getters and setters for each field. -## Example +## API + +### Usage Example ```rust use bitmap::bitmap; @@ -19,16 +20,110 @@ bitmap!( } ); -let mut p = Player(0); -p.set_imposter(1); -p.set_finished_tasks(5); -p.set_kills(3); - -assert_eq!(p.finished_tasks(), 5); +let mut player = Player(0); assert_eq!(std::mem::size_of::(), 1); + +player.set_imposter(1); +player.set_finished_tasks(5); +player.set_kills(3); + +assert_eq!(player.imposter(), 1); +assert_eq!(player.finished_tasks(), 5); +assert_eq!(player.kills(), 3); +assert_eq!(*player, 0b01101011); ``` -# Limitations +### Accessing fields + +For each field `name: T`, where `T` is the smallest possible integer such that +`field_size <= integer.size`, `bitmap!` generates: + +- `fn name(&self) -> T` — returns the value for `name` +- `fn set_name(&mut self, val: T)` — sets the value for `name` + +### Accessing the raw value + +For the struct `Bits(T)`, where `T` is the unsigned integer type used for storage, +the following traits are implemented: + +- `From for T` +- `Deref for Bits`, with `fn deref(&self) -> T` + +```rust +use bitmap::bitmap; + +bitmap!( + struct Bits { + a: u32, + b: u16, + c: u16, + } +); + +let bits = Bits(0); +let underlying_u64: u64 = bits.into(); +let underlying_u64 = *bits; +``` + +### Supported field types: + +```rust +use bitmap::bitmap; + +bitmap!( + struct Bits { + flag: u1, + counter: u7, + } +); +``` -* Total struct size must be ≤ 64 bits (more planned) -* Only `u1` to `u7` types are currently supported (more planned) +Each field must be in the form `uN`, where `1 <= N <= 128`. + +### Maximum total size + +`bitmap!` uses the smallest possible integer type such that `total_bit_width <= integer.bit_width`. +The total bit width must fit into a `u128`. If you need more than that, consider using a `Vec` +of `bitmap`s. + +### Storage order + +Fields are packed from **most significant bit (MSB)** to **least significant bit (LSB)**, matching +big-endian order. + +This means the first declared field is stored in the highest bits of the underlying storage integer. + +```rust +use bitmap::bitmap; + +bitmap!( + struct Bits { + a: u8, + b: u8, + } +); + +let mut bits = Bits(0); +bits.set_a(0xaa) + .set_b(0xbb); + +assert_eq!(*bits, 0xaabb); +``` + +## Note + +`bitmap!` is built with hardware configuration in mind, where most packed bitmaps have a size +aligned to integer sizes. It does not use the _smallest possible size_: a bitmap with only one `u33` +field will take up 64 bits of space. + +```rust +use bitmap::bitmap; + +bitmap!( + struct Bits { + field: u33, + } +); + +assert_eq!(core::mem::size_of::(), 8); +```