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 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); +``` 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(); + } +} diff --git a/macros/src/generator.rs b/macros/src/generator.rs index 575cfe0..98692c9 100644 --- a/macros/src/generator.rs +++ b/macros/src/generator.rs @@ -3,37 +3,70 @@ use quote::quote; use crate::parser::BitmapInput; -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(); +fn get_packed_layout(size: usize) -> Vec { + let usizes = [128, 64, 32, 16, 8]; + let mut remainder = size; + let mut sizes = Vec::::new(); + + for &usz in &usizes { + while remainder >= usz as usize { + sizes.push(usz); + remainder -= usz as usize; + } + } - if size > 64 { - return Err(syn::Error::new_spanned(name, "Too many fields: max supported size is 64 bits")); + if remainder > 0 { + sizes.push(8); } - let storage_ty = match size { + sizes +} + +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!(), - }; + } +} - let mut bit_index = 0; +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: maximum supported size is 128 bits")); + } + + let storage_ty = get_storage_ty(size as u8); + + let mut bit_index = size; let accessors = fields.iter().map(|ident| { - 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; - 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! { - 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 } @@ -43,10 +76,23 @@ 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)* } + + 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 + } + } }) } diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 8714d7b..6f475b0 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -4,24 +4,13 @@ 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` -/// -/// ### Current Limitations -/// - Total bit size must be ≤ 64 bits. -/// - No values larger than `u7` currently supported -/// -/// ### 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. -/// -/// ### Example +/// ### API +/// #### Usage Example /// ``` /// use macros::bitmap; /// @@ -43,7 +32,90 @@ mod parser; /// 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; +/// +/// 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); +/// ``` +/// #[proc_macro] pub fn bitmap(input: TokenStream) -> TokenStream { let parsed = parse_macro_input!(input as parser::BitmapInput); 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/src/lib.rs b/src/lib.rs index d796bfd..29f2605 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] @@ -46,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, @@ -67,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 { @@ -86,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/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/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 { 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, + | ^^