Skip to content

Commit

Permalink
Improved API and documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
haxelion committed Aug 17, 2024
1 parent 9bedc1c commit 42cf143
Show file tree
Hide file tree
Showing 9 changed files with 634 additions and 86 deletions.
98 changes: 94 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,102 @@
[![codecov](https://codecov.io/github/haxelion/bva/graph/badge.svg?token=UMXJD47JCY)](https://codecov.io/github/haxelion/bva)
![Minimum Rust 1.61](https://img.shields.io/badge/Rust-1.61+-red.svg)
[![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
This crate is for manipulating and doing arithmetics on bit vectors of fixed but arbitrary size.
They are meant to behave like CPU hardware registers with wrap-around on overflow.

`bva` is a rust crate for manipulating and doing arithmetics on bit vectors of
fixed but arbitrary size. They are meant to behave like CPU hardware registers with wrap-around on overflow.
This crate provides multiple implementations relying on different memory management strategies.

This crate emphasizes optimizing storage by providing alternative storage options.
The module `fixed` contains implementations using arrays of unsigned integers as storage and thus have a fixed capacity. The module `dynamic` contains an implementation using a dynamically allocated array of integers as storage and therefore has a dynamic capacity. The module `auto` contains an implementation capable of switching at runtime between a fixed or dynamic capacity implementation to try to minimize dynamic memory allocations. All of those implementations implement the `BitVector` trait and can be freely mixed together and used interchangeably via generic traits.
* The `Bvf` implementation uses statically sized arrays of unsigned integers as storage
and thus has a fixed capacity but does not require dynamic memory allocation.
* The `Bvd` implementation uses a dynamically allocated array of
integers as storage and therefore has a dynamic capacity and support resizing operations.
* The `Bv` implementation is capable of switching at runtime between the `Bvf` and `Bvd`
implementations to try to minimize dynamic memory allocations whenever possible.

All of those implementations implement the `BitVector` trait and can be freely mixed together
and abstracted through generic traits.

The different bit vector types represent a vector of bits where the bit at index 0 is the least
significant bit and the bit at index `.len() - 1` is the most significant bit. There is no
notion of endianness for the bit vector themselves, endianness is only involved when reading or
writing a bit vector from or to memory.

Arithmetic operation can be applied to bit vectors of different types and different lengths.
The result will always have the type and the length of the left hand side operand. The right
hand side operand will be zero extended if needed. Operations will wrap-around in the case of
overflows. This should match the behavior of unsigned integer arithmetics on CPU registers.

# Examples

Bit vectors expose an API similar to Rust `std::collections::Vec`:
```rust
use bva::{Bit, BitVector, Bvd};

let mut bv = Bvd::with_capacity(128);
assert_eq!(bv.capacity(), 128);
bv.push(Bit::One);
bv.push(Bit::One);
bv.resize(16, Bit::Zero);
assert_eq!(bv.len(), 16);
assert_eq!(bv.first(), Some(Bit::One));
assert_eq!(bv.last(), Some(Bit::Zero));
let pop_count = bv.iter().fold(0u32, |acc, b| acc + u32::from(b));
assert_eq!(pop_count, 2);
```

Additionally, bit vector specific operations are available:
```rust
use bva::{Bit, BitVector, Bv32};

// While Bv32 has a capacity of 32 bits, it inherits the length of the u8.
let mut a = Bv32::try_from(0b111u8).unwrap();
a.rotr(2);
assert_eq!(a, Bv32::try_from(0b11000001u8).unwrap());
assert_eq!(a.get(7), Bit::One);
a.set(1, Bit::One);
assert_eq!(a, Bv32::try_from(0b11000011u8).unwrap());
assert_eq!(a.copy_range(1..7), Bv32::try_from(0b100001u8).unwrap());
```

Bit vectors behave like unsigned integers with wrap-around on overflow:
```rust
use bva::{Bit, BitVector, Bv32};

// Bv32 is a type alias for a Bvf with 32 bits of capacity.
let a = Bv32::ones(32);
let b = Bv32::try_from(1u32).unwrap();
assert_eq!(b.leading_zeros(), 31);
let c = a + b;
assert_eq!(c, Bv32::zeros(32));
```

Generic traits can be used to abstract over the different bit vector implementations:
```rust
use core::ops::AddAssign;
use bva::{Bit, BitVector, Bv, Bvd, Bvf};

fn fibonnaci<B: BitVector + for<'a> AddAssign<&'a B>>(n: usize) -> B {
let mut f0 = B::zeros(1);
let mut f1 = B::ones(1);
if n == 0 {
return f0;
}

for _ in 1..n {
// Avoid overflow
f0.resize(f1.significant_bits() + 1, Bit::Zero);
// Addition is done in place
f0 += &f1;
// Swap f0 and f1
std::mem::swap(&mut f0, &mut f1);
}
return f1;
}

assert_eq!(fibonnaci::<Bvf<u8, 2>>(15), Bvf::<u8, 2>::try_from(610u16).unwrap());
assert_eq!(fibonnaci::<Bvd>(18), Bvd::from(2584u32));
assert_eq!(fibonnaci::<Bv>(19), Bv::from(4181u32));
```

## Changelog

Expand Down
58 changes: 47 additions & 11 deletions src/auto.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,3 @@
//! This module contains an automatically managed bit vector type. Depending on the required
//! capacity, it might use a fixed capacity implementation to avoid unnecessary dynamic memory
//! allocations, or it might use a dynamic capacity implementation if the capacity of fixed
//! implementations is exceeded.
//!
//! While avoiding memory allocation might improve performance, there is a slight performance cost
//! due to the dynamic dispatch and extra capacity checks. The net effect will depend on the exact
//! application. It is designed for the case where most bit vector are expected to fit inside
//! fixed capacity implementations but some outliers might not.
use std::cmp::Ordering;
use std::fmt::{Binary, Display, LowerHex, Octal, UpperHex};

Expand Down Expand Up @@ -36,10 +26,37 @@ pub(crate) type Bvp = Bv128;
// Bit Vector automatic allocation implementation
// ------------------------------------------------------------------------------------------------

/// A bit vector with automatic capacity management.
/// A bit vector with an automatically managed memory allocation type.
///
/// Depending on the required capacity, it might use a fixed capacity implementation to avoid
/// unnecessary dynamic memory allocations, or it might use a dynamic capacity implementation
/// when needed.
///
/// While avoiding memory allocation might improve performances, there is a slight performance cost
/// due to the dynamic dispatch and extra capacity checks. The net effect will depend on the exact
/// workload.
///
/// # Examples
///
/// ```
/// use bva::{BitVector, Bv};
///
/// // This bit vector will be stack allocated.
/// let a = Bv::from(27u8);
/// assert_eq!(a.len(), 8);
/// // This bit vector will be heap allocated.
/// let b = Bv::ones(256);
/// assert_eq!(b.len(), 256);
/// // The result of this operation will also be heap allocated.
/// let c = b + a;
/// assert_eq!(c.len(), 256);
/// assert_eq!(c.to_string(), "26");
/// ```
#[derive(Clone, Debug)]
pub enum Bv {
#[doc(hidden)]
Fixed(Bvp),
#[doc(hidden)]
Dynamic(Bvd),
}

Expand All @@ -49,6 +66,15 @@ impl Bv {
/// length of the bit vector.
///
/// Calling this method might cause the storage to become dynamically allocated.
///
/// ```
/// use bva::{BitVector, Bv};
///
/// let mut bv = Bv::ones(128);
/// assert_eq!(bv.capacity(), 128);
/// bv.reserve(128);
/// assert_eq!(bv.capacity(), 256);
/// ```
pub fn reserve(&mut self, additional: usize) {
match self {
&mut Bv::Fixed(ref b) => {
Expand All @@ -66,6 +92,16 @@ impl Bv {
///
/// Calling this method might cause the implementation to drop unnecessary dynamically
/// allocated memory.
///
/// ```
/// use bva::{BitVector, Bv};
///
/// let mut bv = Bv::ones(128);
/// bv.reserve(128);
/// assert_eq!(bv.capacity(), 256);
/// bv.shrink_to_fit();
/// assert_eq!(bv.capacity(), 128);
/// ```
pub fn shrink_to_fit(&mut self) {
if let Bv::Dynamic(b) = self {
if b.len() <= Bvp::capacity() {
Expand Down
25 changes: 22 additions & 3 deletions src/bit.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
//! Implementation of the [`Bit`] type.
//!
//! This module contains a more natural representation for bits in the form of an enumeration.
//!
//! # Examples
//!
//! ```
//! use bva::Bit;
//!
//! assert_eq!(Bit::Zero, 0u8.into());
//! assert_eq!(true, Bit::One.into());
//! ```
use std::fmt::Display;

/// Enumeration representing a single bit.
/// A single bit.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Bit {
/// The bit `0`.
Zero,
/// The bit `1`.
One,
}

macro_rules! bit_from_impl {
($($t:ty)*) => ($(
impl From<Bit> for $t {
/// Returns `0` if `bit` is `Bit::Zero` and `1` if `bit` is `Bit::One`.
fn from(bit: Bit) -> $t {
match bit {
Bit::Zero => 0,
Expand All @@ -19,6 +35,7 @@ macro_rules! bit_from_impl {
}

impl From<$t> for Bit {
/// Returns `Bit::Zero` if `u` is `0` and `Bit::One` for any other value of `u`.
fn from(u: $t) -> Bit {
match u {
0 => Bit::Zero,
Expand All @@ -29,7 +46,10 @@ macro_rules! bit_from_impl {
)*)
}

bit_from_impl! {u8 u16 u32 u64 u128 usize}

impl From<Bit> for bool {
/// Returns `false` if `bit` is `Bit::Zero` and `true` if `bit` is `Bit::One`.
fn from(bit: Bit) -> bool {
match bit {
Bit::Zero => false,
Expand All @@ -39,6 +59,7 @@ impl From<Bit> for bool {
}

impl From<bool> for Bit {
/// Returns `Bit::Zero` if `b` is `false` and `Bit::One` if `b` is `true`.
fn from(b: bool) -> Bit {
match b {
false => Bit::Zero,
Expand All @@ -47,8 +68,6 @@ impl From<bool> for Bit {
}
}

bit_from_impl! {u8 u16 u32 u64 u128 usize}

impl Display for Bit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Expand Down
43 changes: 32 additions & 11 deletions src/dynamic.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
//! This module contains a dynamic capacity bit vector implementation using a dynamically allocated
//! integer array as storage.
//!
//! As the capacity is dynamic, performing operations exceeding the current capacity will result in
//! a reallocation of the internal array.
use std::cmp::Ordering;
use std::fmt;
use std::io::{Read, Write};
Expand All @@ -24,7 +18,10 @@ use crate::{Bit, BitVector, ConvertionError, Endianness};
// Bit Vector Dynamic allocation implementation
// ------------------------------------------------------------------------------------------------

/// A bit vector with dynamic capacity.
/// A bit vector using a dynamically allocated (heap allocated) memory implementation.
///
/// As the capacity is dynamic, performing operations exceeding the current capacity will result in
/// a reallocation of the internal array.
#[derive(Clone, Debug)]
pub struct Bvd {
data: Box<[u64]>,
Expand All @@ -36,10 +33,25 @@ impl Bvd {
const NIBBLE_UNIT: usize = size_of::<u64>() * 2;
const BIT_UNIT: usize = u64::BITS as usize;

/// Construct a new [`Bvd`] with the given data and length.
/// The least significant bit will be the least significant bit of the first `u64`
/// and the most significant bit will be the most significant bit of the last `u64`.
/// This is a low level function and should be used with care, prefer using the
/// functions of the [`BitVector`] trait.
///
/// ```
/// use bva::Bvd;
///
/// let data = vec![0x0000_0000_0000_0001, 0x7000_0000_0000_0000];
/// let bv = Bvd::new(data.into_boxed_slice(), 128);
/// assert_eq!(bv, Bvd::from(0x7000_0000_0000_0000_0000_0000_0000_0001u128));
/// ```
pub const fn new(data: Box<[u64]>, length: usize) -> Self {
assert!(length <= data.len() * Self::BIT_UNIT);
Self { data, length }
}

/// Deconstruct a [`Bvd`] into its inner data and length.
pub fn into_inner(self) -> (Box<[u64]>, usize) {
(self.data, self.length)
}
Expand All @@ -57,6 +69,15 @@ impl Bvd {
/// length of the bit vector.
///
/// The underlying allocator might reserve additional capacity.
///
/// ```
/// use bva::{BitVector, Bvd};
///
/// let mut bv = Bvd::zeros(64);
/// assert_eq!(bv.capacity(), 64);
/// bv.reserve(64);
/// assert!(bv.capacity() == 128);
/// ```
pub fn reserve(&mut self, additional: usize) {
let new_capacity = self.length + additional;
if Self::capacity_from_bit_len(new_capacity) > self.data.len() {
Expand Down Expand Up @@ -231,14 +252,14 @@ impl BitVector for Bvd {
.take(Self::capacity_from_byte_len(byte_length))
.collect();
match endianness {
Endianness::LE => {
Endianness::Little => {
let offset = (Self::BYTE_UNIT - byte_length % Self::BYTE_UNIT) % Self::BYTE_UNIT;
for (i, b) in bytes.as_ref().iter().rev().enumerate() {
let j = data.len() - 1 - (i + offset) / Self::BYTE_UNIT;
data[j] = (data[j] << 8) | *b as u64;
}
}
Endianness::BE => {
Endianness::Big => {
let offset = (Self::BYTE_UNIT - byte_length % Self::BYTE_UNIT) % Self::BYTE_UNIT;
for (i, b) in bytes.as_ref().iter().enumerate() {
let j = data.len() - 1 - (i + offset) / Self::BYTE_UNIT;
Expand All @@ -256,13 +277,13 @@ impl BitVector for Bvd {
let num_bytes = (self.length + 7) / 8;
let mut buf: Vec<u8> = repeat(0u8).take(num_bytes).collect();
match endianness {
Endianness::LE => {
Endianness::Little => {
for i in 0..num_bytes {
buf[i] = (self.data[i / Self::BYTE_UNIT] >> ((i % Self::BYTE_UNIT) * 8) & 0xff)
as u8;
}
}
Endianness::BE => {
Endianness::Big => {
for i in 0..num_bytes {
buf[num_bytes - i - 1] = (self.data[i / Self::BYTE_UNIT]
>> ((i % Self::BYTE_UNIT) * 8)
Expand Down
Loading

0 comments on commit 42cf143

Please sign in to comment.