commonware_utils/bitmap/historical/bitmap.rs
1use super::Error;
2use crate::bitmap::Prunable;
3#[cfg(not(feature = "std"))]
4use alloc::{collections::BTreeMap, vec::Vec};
5#[cfg(feature = "std")]
6use std::collections::BTreeMap;
7
8/// Sealed trait for bitmap state types.
9mod private {
10 pub trait Sealed {}
11}
12
13/// Trait for valid bitmap type states [Clean], [Dirty].
14pub trait State: private::Sealed + Sized + Send + Sync {}
15
16/// Clean bitmap type state - bitmap has no pending mutations.
17#[derive(Clone, Debug)]
18pub struct Clean;
19
20impl private::Sealed for Clean {}
21impl State for Clean {}
22
23/// Dirty bitmap type state - bitmap has pending mutations not yet committed.
24///
25/// # De-duplication and Cancellation
26///
27/// **The dirty state de-duplicates during operations, not at commit time.**
28///
29/// Operations that cancel out are handled automatically:
30///
31/// ```text
32/// Example 1: push + pop = no-op
33/// push(true) → appended_bits=[true], projected_len=11
34/// pop() → appended_bits=[], projected_len=10
35/// Result: dirty state unchanged from base
36///
37/// Example 2: set_bit + set_bit = last write wins
38/// set_bit(5, true) → modified_bits={5: true}
39/// set_bit(5, false) → modified_bits={5: false}
40/// Result: only final value recorded
41///
42/// Example 3: set_bit + pop = cancels modification
43/// set_bit(9, true) → modified_bits={9: true}
44/// pop() → modified_bits={} (removed), projected_len=9
45/// Result: bit 9 no longer exists, modification discarded
46/// ```
47///
48/// # Key Invariants
49///
50/// 1. **Base immutability**: `base_len` and `base_pruned_chunks` never change
51/// 2. **Appended region**: Always occupies `[projected_len - appended_bits.len(), projected_len)`
52/// 3. **Modified region**: `modified_bits` only contains offsets in `[0, projected_len - appended_bits.len())`
53/// - These are modifications to the base bitmap, never to appended bits
54/// - Appended bits are modified by directly updating the `appended_bits` vector
55/// 4. **No overlap**: A bit is either in `modified_bits` OR `appended_bits`, never both
56#[derive(Clone, Debug)]
57pub struct Dirty<const N: usize> {
58 /// Bitmap state when dirty started (immutable).
59 base_len: u64,
60 base_pruned_chunks: usize,
61
62 /// What the bitmap will look like after commit (mutable).
63 projected_len: u64,
64 projected_pruned_chunks: usize,
65
66 /// Modifications to bits that existed in the bitmap (not appended bits).
67 /// Contains offsets in [0, projected_len - appended_bits.len()).
68 /// Maps: bit -> new_value
69 modified_bits: BTreeMap<u64, bool>,
70
71 /// New bits pushed in this dirty state (in order).
72 /// Logical position: [projected_len - appended_bits.len(), projected_len)
73 appended_bits: Vec<bool>,
74
75 /// Old chunk data for chunks being pruned.
76 /// Captured eagerly during `prune_to_bit()` for historical reconstruction.
77 chunks_to_prune: BTreeMap<usize, [u8; N]>,
78}
79
80impl<const N: usize> private::Sealed for Dirty<N> {}
81impl<const N: usize> State for Dirty<N> {}
82
83/// A change to a chunk.
84#[derive(Clone, Debug)]
85pub(super) enum ChunkDiff<const N: usize> {
86 /// Chunk was modified (contains old value before the change).
87 Modified([u8; N]),
88 /// Chunk was removed from the right side (contains old value before removal).
89 Removed([u8; N]),
90 /// Chunk was added (did not exist before).
91 Added,
92 /// Chunk was pruned from the left side (contains old value before pruning).
93 Pruned([u8; N]),
94}
95
96/// A reverse diff that describes the state before a commit.
97#[derive(Clone, Debug)]
98pub(super) struct CommitDiff<const N: usize> {
99 /// Total length in bits before this commit.
100 pub(super) len: u64,
101 /// Number of pruned chunks before this commit.
102 pub(super) pruned_chunks: usize,
103 /// Chunk-level changes.
104 pub(super) chunk_diffs: BTreeMap<usize, ChunkDiff<N>>,
105}
106
107/// A historical bitmap that maintains one actual bitmap plus diffs for history.
108///
109/// Uses a type-state pattern to track whether the bitmap is clean (no pending
110/// mutations) or dirty (has pending mutations).
111///
112/// Commit numbers must be strictly monotonically increasing and < u64::MAX.
113#[derive(Clone, Debug)]
114pub struct BitMap<const N: usize, S: State = Clean> {
115 /// The current/HEAD state - the one and only full bitmap.
116 current: Prunable<N>,
117
118 /// Historical commits: commit_number -> reverse diff from that commit.
119 commits: BTreeMap<u64, CommitDiff<N>>,
120
121 /// State marker (Clean or Dirty).
122 state: S,
123}
124
125/// Type alias for a clean bitmap with no pending mutations.
126pub type CleanBitMap<const N: usize> = BitMap<N, Clean>;
127
128/// Type alias for a dirty bitmap with pending mutations.
129pub type DirtyBitMap<const N: usize> = BitMap<N, Dirty<N>>;
130
131impl<const N: usize> CleanBitMap<N> {
132 /// Create a new empty historical bitmap.
133 pub const fn new() -> Self {
134 Self {
135 current: Prunable::new(),
136 commits: BTreeMap::new(),
137 state: Clean,
138 }
139 }
140
141 /// Create a new historical bitmap with the given number of pruned chunks.
142 pub fn new_with_pruned_chunks(pruned_chunks: usize) -> Result<Self, Error> {
143 Ok(Self {
144 current: Prunable::new_with_pruned_chunks(pruned_chunks)?,
145 commits: BTreeMap::new(),
146 state: Clean,
147 })
148 }
149
150 /// Transition to dirty state to begin making mutations.
151 ///
152 /// All mutations are applied to a diff layer and do not affect the current
153 /// bitmap until commit.
154 ///
155 /// # Examples
156 ///
157 /// ```
158 /// # use commonware_utils::bitmap::historical::BitMap;
159 /// let bitmap: BitMap<4> = BitMap::new();
160 ///
161 /// let mut dirty = bitmap.into_dirty();
162 /// dirty.push(true);
163 /// dirty.push(false);
164 /// let bitmap = dirty.commit(1).unwrap();
165 ///
166 /// assert_eq!(bitmap.len(), 2);
167 /// ```
168 pub fn into_dirty(self) -> DirtyBitMap<N> {
169 DirtyBitMap {
170 state: Dirty {
171 base_len: self.current.len(),
172 base_pruned_chunks: self.current.pruned_chunks(),
173 projected_len: self.current.len(),
174 projected_pruned_chunks: self.current.pruned_chunks(),
175 modified_bits: BTreeMap::new(),
176 appended_bits: Vec::new(),
177 chunks_to_prune: BTreeMap::new(),
178 },
179 current: self.current,
180 commits: self.commits,
181 }
182 }
183
184 /// Execute a closure with a dirty bitmap and commit it at the given commit number.
185 ///
186 /// # Errors
187 ///
188 /// Returns [Error::NonMonotonicCommit] if the commit number is not
189 /// greater than the previous commit.
190 ///
191 /// Returns [Error::ReservedCommitNumber] if the commit number is `u64::MAX`.
192 ///
193 /// # Examples
194 ///
195 /// ```
196 /// # use commonware_utils::bitmap::historical::BitMap;
197 /// let mut bitmap: BitMap<4> = BitMap::new();
198 ///
199 /// bitmap = bitmap.apply_batch(1, |dirty| {
200 /// dirty.push(true).push(false);
201 /// }).unwrap();
202 ///
203 /// assert_eq!(bitmap.len(), 2);
204 /// ```
205 pub fn apply_batch<F>(self, commit_number: u64, f: F) -> Result<Self, Error>
206 where
207 F: FnOnce(&mut DirtyBitMap<N>),
208 {
209 let mut dirty = self.into_dirty();
210 f(&mut dirty);
211 dirty.commit(commit_number)
212 }
213
214 /// Get the bitmap state as it existed at a specific commit.
215 ///
216 /// Returns `None` if the commit does not exist or if `commit_number` is `u64::MAX`
217 /// (which is reserved and cannot be used as a commit number).
218 ///
219 /// This reconstructs the historical state by applying reverse diffs backward from
220 /// the current state. Each commit's reverse diff describes the state before that
221 /// commit, so we "undo" commits one by one until we reach the target.
222 ///
223 /// # Examples
224 ///
225 /// ```
226 /// # use commonware_utils::bitmap::historical::BitMap;
227 /// let mut bitmap: BitMap<4> = BitMap::new();
228 ///
229 /// bitmap = bitmap.apply_batch(1, |dirty| {
230 /// dirty.push(true);
231 /// dirty.push(false);
232 /// }).unwrap();
233 ///
234 /// bitmap = bitmap.apply_batch(2, |dirty| {
235 /// dirty.set_bit(0, false);
236 /// dirty.push(true);
237 /// }).unwrap();
238 ///
239 /// // Get state as it was at commit 1
240 /// let state_at_1 = bitmap.get_at_commit(1).unwrap();
241 /// assert_eq!(state_at_1.len(), 2);
242 /// assert!(state_at_1.get_bit(0));
243 /// assert!(!state_at_1.get_bit(1));
244 ///
245 /// // Current state is different
246 /// assert_eq!(bitmap.len(), 3);
247 /// assert!(!bitmap.get_bit(0));
248 /// ```
249 pub fn get_at_commit(&self, commit_number: u64) -> Option<Prunable<N>> {
250 // Check if the commit exists and is valid
251 if commit_number == u64::MAX || !self.commits.contains_key(&commit_number) {
252 return None;
253 }
254
255 // Start with current state
256 let mut state = self.current.clone();
257
258 // Apply reverse diffs from newest down to target (exclusive)
259 // Each reverse diff at commit N describes the state before commit N
260 // Addition can't overflow because commit_number < u64::MAX
261 for (_commit, diff) in self.commits.range(commit_number + 1..).rev() {
262 self.apply_reverse_diff(&mut state, diff);
263 }
264
265 Some(state)
266 }
267
268 /// Check if a commit exists.
269 pub fn commit_exists(&self, commit_number: u64) -> bool {
270 self.commits.contains_key(&commit_number)
271 }
272
273 /// Get an iterator over all commit numbers in ascending order.
274 pub fn commits(&self) -> impl Iterator<Item = u64> + '_ {
275 self.commits.keys().copied()
276 }
277
278 /// Get the latest commit number, if any commits exist.
279 pub fn latest_commit(&self) -> Option<u64> {
280 self.commits.keys().next_back().copied()
281 }
282
283 /// Get the earliest commit number, if any commits exist.
284 pub fn earliest_commit(&self) -> Option<u64> {
285 self.commits.keys().next().copied()
286 }
287
288 /// Get a reference to the current bitmap state.
289 pub const fn current(&self) -> &Prunable<N> {
290 &self.current
291 }
292
293 /// Number of bits in the current bitmap.
294 #[inline]
295 pub const fn len(&self) -> u64 {
296 self.current.len()
297 }
298
299 /// Returns true if the current bitmap is empty.
300 #[inline]
301 pub const fn is_empty(&self) -> bool {
302 self.current.is_empty()
303 }
304
305 /// Get the value of a bit in the current bitmap.
306 #[inline]
307 pub fn get_bit(&self, bit: u64) -> bool {
308 self.current.get_bit(bit)
309 }
310
311 /// Get the chunk containing a bit in the current bitmap.
312 #[inline]
313 pub fn get_chunk_containing(&self, bit: u64) -> &[u8; N] {
314 self.current.get_chunk_containing(bit)
315 }
316
317 /// Number of pruned chunks in the current bitmap.
318 #[inline]
319 pub const fn pruned_chunks(&self) -> usize {
320 self.current.pruned_chunks()
321 }
322
323 /// Remove all commits with numbers below the commit number.
324 ///
325 /// Returns the number of commits removed.
326 pub fn prune_commits_before(&mut self, commit_number: u64) -> usize {
327 let count = self.commits.len();
328 self.commits = self.commits.split_off(&commit_number);
329 count - self.commits.len()
330 }
331
332 /// Clear all historical commits.
333 pub fn clear_history(&mut self) {
334 self.commits.clear();
335 }
336
337 /// Push bits to extend the bitmap to target length.
338 fn push_to_length(&self, state: &mut Prunable<N>, target_len: u64) {
339 while state.len() < target_len {
340 let remaining = target_len - state.len();
341 let next_bit = state.len() % Prunable::<N>::CHUNK_SIZE_BITS;
342
343 // If we're at a chunk boundary and need at least a full chunk, push an entire chunk
344 if next_bit == 0 && remaining >= Prunable::<N>::CHUNK_SIZE_BITS {
345 state.push_chunk(&[0u8; N]);
346 } else {
347 // Otherwise push individual bits
348 state.push(false);
349 }
350 }
351 }
352
353 /// Pop bits to shrink the bitmap to target length.
354 /// Optimized to pop entire chunks when possible.
355 fn pop_to_length(&self, state: &mut Prunable<N>, target_len: u64) {
356 while state.len() > target_len {
357 let excess = state.len() - target_len;
358 let next_bit = state.len() % Prunable::<N>::CHUNK_SIZE_BITS;
359
360 // If at chunk boundary and we need to remove at least a full chunk, pop entire chunk
361 if next_bit == 0 && excess >= Prunable::<N>::CHUNK_SIZE_BITS {
362 state.pop_chunk();
363 } else {
364 // Otherwise pop individual bits
365 state.pop();
366 }
367 }
368 }
369
370 /// Apply a reverse diff to transform newer_state into the previous state (in-place).
371 ///
372 /// Algorithm:
373 /// 1. Restore pruned chunks by prepending them back (unprune)
374 /// 2. Adjust bitmap structure to target length (extend/shrink as needed)
375 /// 3. Update chunk data for Modified and Removed chunks
376 /// 4. Set next_bit to match target length exactly
377 fn apply_reverse_diff(&self, newer_state: &mut Prunable<N>, diff: &CommitDiff<N>) {
378 let target_len = diff.len;
379 let target_pruned = diff.pruned_chunks;
380 let newer_pruned = newer_state.pruned_chunks();
381
382 // Phase 1: Restore pruned chunks
383 assert!(
384 target_pruned <= newer_pruned,
385 "invariant violation: target_pruned ({target_pruned}) > newer_pruned ({newer_pruned})"
386 );
387 let mut chunks_to_unprune = Vec::with_capacity(newer_pruned - target_pruned);
388 for chunk_index in (target_pruned..newer_pruned).rev() {
389 let Some(ChunkDiff::Pruned(chunk)) = diff.chunk_diffs.get(&chunk_index) else {
390 panic!("chunk {chunk_index} should be Pruned in diff");
391 };
392 chunks_to_unprune.push(*chunk);
393 }
394 newer_state.unprune_chunks(&chunks_to_unprune);
395
396 // Phase 2: Adjust bitmap structure to target length
397 if newer_state.len() < target_len {
398 self.push_to_length(newer_state, target_len);
399 } else if newer_state.len() > target_len {
400 self.pop_to_length(newer_state, target_len);
401 }
402
403 // Phase 3: Update chunk data
404 for (&chunk_index, change) in diff
405 .chunk_diffs
406 .iter()
407 .filter(|(chunk_index, _)| **chunk_index >= newer_pruned)
408 {
409 match change {
410 ChunkDiff::Modified(old_data) | ChunkDiff::Removed(old_data) => {
411 // Both cases: chunk exists in target, just update its data
412 newer_state.set_chunk_by_index(chunk_index, old_data);
413 }
414 ChunkDiff::Added => {
415 // Chunk didn't exist in target - already handled by pop_to_length.
416 // We can break here because there are no more modifications to apply.
417 // Added can only occur after all Modified. If we encounter Added, we know
418 // there are no Removed. (diff.chunk_diffs can't have both Added and Removed.)
419 break;
420 }
421 ChunkDiff::Pruned(_) => {
422 panic!("pruned chunk found at unexpected index {chunk_index}")
423 }
424 }
425 }
426
427 assert_eq!(newer_state.pruned_chunks(), target_pruned);
428 assert_eq!(newer_state.len(), target_len);
429 }
430}
431
432impl<const N: usize> Default for CleanBitMap<N> {
433 fn default() -> Self {
434 Self::new()
435 }
436}
437
438impl<const N: usize> DirtyBitMap<N> {
439 /// Get the length of the bitmap as it would be after committing.
440 #[inline]
441 pub const fn len(&self) -> u64 {
442 self.state.projected_len
443 }
444
445 /// Returns true if the bitmap would be empty after committing.
446 #[inline]
447 pub const fn is_empty(&self) -> bool {
448 self.len() == 0
449 }
450
451 /// Get the number of pruned chunks after committing.
452 #[inline]
453 pub const fn pruned_chunks(&self) -> usize {
454 self.state.projected_pruned_chunks
455 }
456
457 /// Get a bit value with read-through semantics.
458 ///
459 /// Returns the bit's value as it would be after committing.
460 /// Priority: appended bits > modified bits > original bitmap.
461 ///
462 /// # Panics
463 ///
464 /// Panics if the bit offset is out of bounds or if the bit has been pruned.
465 pub fn get_bit(&self, bit: u64) -> bool {
466 assert!(
467 bit < self.state.projected_len,
468 "bit offset {bit} out of bounds (len: {})",
469 self.state.projected_len
470 );
471
472 let chunk_idx = Prunable::<N>::unpruned_chunk(bit);
473 assert!(
474 chunk_idx >= self.state.projected_pruned_chunks,
475 "cannot get bit {bit}: chunk {chunk_idx} is pruned (pruned up to chunk {})",
476 self.state.projected_pruned_chunks
477 );
478
479 // Priority 1: Check if bit is in appended region.
480 // Must use appended_start, not base_len, to handle net pops + appends.
481 let appended_start = self.state.projected_len - self.state.appended_bits.len() as u64;
482 if bit >= appended_start {
483 let append_offset = (bit - appended_start) as usize;
484 return self.state.appended_bits[append_offset];
485 }
486
487 // Priority 2: Check if bit was modified.
488 if let Some(&value) = self.state.modified_bits.get(&bit) {
489 return value;
490 }
491
492 // Priority 3: Fall through to original bitmap.
493 self.current.get_bit(bit)
494 }
495
496 /// Get a chunk value with read-through semantics.
497 ///
498 /// Reconstructs the chunk if it has modifications, otherwise returns from current.
499 ///
500 /// # Panics
501 ///
502 /// Panics if the bit offset is out of bounds or if the chunk has been pruned.
503 pub fn get_chunk(&self, bit: u64) -> [u8; N] {
504 // Check bounds
505 assert!(
506 bit < self.state.projected_len,
507 "bit offset {bit} out of bounds (len: {})",
508 self.state.projected_len
509 );
510
511 let chunk_idx = Prunable::<N>::unpruned_chunk(bit);
512
513 // Check if chunk is in pruned range
514 assert!(
515 chunk_idx >= self.state.projected_pruned_chunks,
516 "cannot get chunk at bit offset {bit}: chunk {chunk_idx} is pruned (pruned up to chunk {})",
517 self.state.projected_pruned_chunks
518 );
519
520 let chunk_start_bit = chunk_idx as u64 * Prunable::<N>::CHUNK_SIZE_BITS;
521 let chunk_end_bit = chunk_start_bit + Prunable::<N>::CHUNK_SIZE_BITS;
522
523 // Determine if this chunk needs reconstruction.
524 let appended_start = self.state.projected_len - self.state.appended_bits.len() as u64;
525
526 // Skip reconstruction only if chunk is entirely outside modified regions
527 let chunk_entirely_past_end = chunk_start_bit >= self.state.projected_len;
528 let chunk_entirely_before_changes =
529 chunk_end_bit <= appended_start && chunk_end_bit <= self.state.projected_len;
530
531 let chunk_needs_reconstruction =
532 // Chunk overlaps with pops or appends
533 !(chunk_entirely_past_end || chunk_entirely_before_changes)
534 // OR chunk has explicit bit modifications
535 || (chunk_start_bit..chunk_end_bit.min(self.state.base_len))
536 .any(|bit| self.state.modified_bits.contains_key(&bit));
537
538 if chunk_needs_reconstruction {
539 // Reconstruct chunk from current + modifications
540 self.reconstruct_modified_chunk(chunk_start_bit)
541 } else {
542 // Fall through to current bitmap
543 *self.current.get_chunk_containing(bit)
544 }
545 }
546
547 /// Reconstruct a chunk that has modifications, appends, or pops.
548 fn reconstruct_modified_chunk(&self, chunk_start: u64) -> [u8; N] {
549 // Start with current chunk if it exists
550 let mut chunk = if chunk_start < self.current.len() {
551 *self.current.get_chunk_containing(chunk_start)
552 } else {
553 [0u8; N]
554 };
555
556 // Calculate appended region boundary
557 let appended_start = self.state.projected_len - self.state.appended_bits.len() as u64;
558
559 // Apply modifications and zero out popped bits
560 for bit_in_chunk in 0..Prunable::<N>::CHUNK_SIZE_BITS {
561 let bit = chunk_start + bit_in_chunk;
562
563 let byte_idx = (bit_in_chunk / 8) as usize;
564 let bit_idx = bit_in_chunk % 8;
565 let mask = 1u8 << bit_idx;
566
567 if bit >= self.state.projected_len {
568 // Bit is beyond projected length (popped), zero it out
569 chunk[byte_idx] &= !mask;
570 } else if let Some(&value) = self.state.modified_bits.get(&bit) {
571 // Bit was explicitly modified
572 if value {
573 chunk[byte_idx] |= mask;
574 } else {
575 chunk[byte_idx] &= !mask;
576 }
577 } else if bit >= appended_start {
578 // This is an appended bit
579 let append_offset = (bit - appended_start) as usize;
580 if append_offset < self.state.appended_bits.len() {
581 let value = self.state.appended_bits[append_offset];
582 if value {
583 chunk[byte_idx] |= mask;
584 } else {
585 chunk[byte_idx] &= !mask;
586 }
587 }
588 }
589 }
590
591 chunk
592 }
593
594 /// Set a bit value.
595 ///
596 /// # Panics
597 ///
598 /// Panics if the bit offset is out of bounds or if the bit has been pruned.
599 pub fn set_bit(&mut self, bit: u64, value: bool) -> &mut Self {
600 assert!(
601 bit < self.state.projected_len,
602 "cannot set bit {bit}: out of bounds (len: {})",
603 self.state.projected_len
604 );
605
606 let chunk_idx = Prunable::<N>::unpruned_chunk(bit);
607 assert!(
608 chunk_idx >= self.state.projected_pruned_chunks,
609 "cannot set bit {bit}: chunk {chunk_idx} is pruned (pruned up to chunk {})",
610 self.state.projected_pruned_chunks
611 );
612
613 // Determine which region this bit belongs to.
614 // Appended region: bits pushed, starting at projected_len - appended_bits.len()
615 let appended_start = self.state.projected_len - self.state.appended_bits.len() as u64;
616
617 if bit >= appended_start {
618 // Bit is in the appended region: update the appended_bits vector directly.
619 let append_offset = (bit - appended_start) as usize;
620 self.state.appended_bits[append_offset] = value;
621 } else {
622 // Bit is in the base region: record as a modification.
623 self.state.modified_bits.insert(bit, value);
624 }
625
626 self
627 }
628
629 /// Push a bit to the end of the bitmap.
630 pub fn push(&mut self, bit: bool) -> &mut Self {
631 self.state.appended_bits.push(bit);
632 self.state.projected_len += 1;
633 self
634 }
635
636 /// Push a byte to the end of the bitmap.
637 pub fn push_byte(&mut self, byte: u8) -> &mut Self {
638 for i in 0..8 {
639 let bit = (byte >> i) & 1 == 1;
640 self.push(bit);
641 }
642 self
643 }
644
645 /// Push a full chunk to the end of the bitmap.
646 pub fn push_chunk(&mut self, chunk: &[u8; N]) -> &mut Self {
647 for byte in chunk {
648 self.push_byte(*byte);
649 }
650 self
651 }
652
653 /// Pop the last bit from the bitmap.
654 ///
655 /// Returns the value of the popped bit, accounting for any modifications.
656 ///
657 /// # Panics
658 ///
659 /// Panics if the bitmap is empty.
660 pub fn pop(&mut self) -> bool {
661 assert!(self.state.projected_len > 0, "cannot pop from empty bitmap");
662
663 let old_projected_len = self.state.projected_len;
664 self.state.projected_len -= 1;
665 let bit = self.state.projected_len;
666
667 // Determine which region the popped bit came from.
668 // The appended region contains bits pushed: [appended_start, old_projected_len)
669 let appended_start = old_projected_len - self.state.appended_bits.len() as u64;
670
671 if bit >= appended_start {
672 // Popping from appended region: remove from appended_bits vector.
673 self.state.appended_bits.pop().unwrap()
674 } else {
675 // Popping from base region: check if it was modified.
676 if let Some(&modified_value) = self.state.modified_bits.get(&bit) {
677 self.state.modified_bits.remove(&bit);
678 modified_value
679 } else {
680 // Not modified, return original value.
681 self.current.get_bit(bit)
682 }
683 }
684 }
685
686 /// Prune chunks up to the chunk containing the given bit offset.
687 ///
688 /// Note: `bit` can equal `projected_len` when pruning at a chunk boundary.
689 ///
690 /// # Panics
691 ///
692 /// Panics if `bit` is > the projected length.
693 pub fn prune_to_bit(&mut self, bit: u64) -> &mut Self {
694 assert!(
695 bit <= self.state.projected_len,
696 "cannot prune to bit {bit}: beyond projected length ({})",
697 self.state.projected_len
698 );
699
700 let chunk_num = Prunable::<N>::unpruned_chunk(bit);
701
702 if chunk_num <= self.state.projected_pruned_chunks {
703 return self; // Already pruned
704 }
705
706 // Capture preimages of chunks being pruned
707 let current_pruned = self.current.pruned_chunks();
708 for chunk_idx in self.state.projected_pruned_chunks..chunk_num {
709 if self.state.chunks_to_prune.contains_key(&chunk_idx) {
710 continue; // Already captured
711 }
712
713 // Invariant: chunk_idx should always be >= current_pruned because
714 // projected_pruned_chunks starts at base_pruned_chunks (= current_pruned)
715 assert!(
716 chunk_idx >= current_pruned,
717 "attempting to prune chunk {chunk_idx} which is already pruned (current pruned_chunks={current_pruned})",
718 );
719
720 let bitmap_idx = chunk_idx - current_pruned;
721
722 // Get chunk data, which may come from dirty state if it's appended
723 let chunk_data = if bitmap_idx < self.current.chunks_len() {
724 // Chunk exists in current bitmap
725 *self.current.get_chunk(bitmap_idx)
726 } else {
727 // Chunk only exists in appended bits
728 // Manually reconstruct it from appended_bits
729 let chunk_start_bit = chunk_idx as u64 * Prunable::<N>::CHUNK_SIZE_BITS;
730 let appended_start =
731 self.state.projected_len - self.state.appended_bits.len() as u64;
732
733 let mut chunk = [0u8; N];
734 for bit_in_chunk in 0..Prunable::<N>::CHUNK_SIZE_BITS {
735 let bit = chunk_start_bit + bit_in_chunk;
736 if bit >= self.state.projected_len {
737 break;
738 }
739 if bit >= appended_start {
740 let append_idx = (bit - appended_start) as usize;
741 if append_idx < self.state.appended_bits.len()
742 && self.state.appended_bits[append_idx]
743 {
744 let byte_idx = (bit_in_chunk / 8) as usize;
745 let bit_idx = bit_in_chunk % 8;
746 chunk[byte_idx] |= 1u8 << bit_idx;
747 }
748 }
749 }
750 chunk
751 };
752
753 self.state.chunks_to_prune.insert(chunk_idx, chunk_data);
754 }
755
756 self.state.projected_pruned_chunks = chunk_num;
757
758 self
759 }
760
761 /// Commit the changes and return a clean bitmap with a historical snapshot.
762 ///
763 /// # Errors
764 ///
765 /// Returns [Error::NonMonotonicCommit] if the commit number is not
766 /// greater than the previous commit.
767 ///
768 /// Returns [Error::ReservedCommitNumber] if the commit number is `u64::MAX`.
769 pub fn commit(mut self, commit_number: u64) -> Result<CleanBitMap<N>, Error> {
770 // Validate commit number is not reserved
771 if commit_number == u64::MAX {
772 return Err(Error::ReservedCommitNumber);
773 }
774
775 // Validate commit number is monotonically increasing
776 if let Some(&max_commit) = self.commits.keys().next_back() {
777 if commit_number <= max_commit {
778 return Err(Error::NonMonotonicCommit {
779 previous: max_commit,
780 attempted: commit_number,
781 });
782 }
783 }
784
785 // Build reverse diff (captures OLD state before applying changes)
786 let reverse_diff = self.build_reverse_diff();
787
788 // Shrink to length before appends (handles net pops)
789 let target_len_before_appends =
790 self.state.projected_len - self.state.appended_bits.len() as u64;
791 while self.current.len() > target_len_before_appends {
792 self.current.pop();
793 }
794 // Grow by appending new bits
795 for &bit in &self.state.appended_bits {
796 self.current.push(bit);
797 }
798 assert_eq!(self.current.len(), self.state.projected_len);
799 // Modify existing base bits (not appended bits)
800 for (&bit, &value) in &self.state.modified_bits {
801 self.current.set_bit(bit, value);
802 }
803 // Prune chunks from the beginning
804 if self.state.projected_pruned_chunks > self.state.base_pruned_chunks {
805 let prune_to_bit =
806 self.state.projected_pruned_chunks as u64 * Prunable::<N>::CHUNK_SIZE_BITS;
807 self.current.prune_to_bit(prune_to_bit);
808 }
809
810 // Store the reverse diff
811 self.commits.insert(commit_number, reverse_diff);
812
813 Ok(CleanBitMap {
814 current: self.current,
815 commits: self.commits,
816 state: Clean,
817 })
818 }
819
820 /// Abort the changes and return to clean state.
821 ///
822 /// All pending mutations are discarded.
823 pub fn abort(self) -> CleanBitMap<N> {
824 CleanBitMap {
825 current: self.current,
826 commits: self.commits,
827 state: Clean,
828 }
829 }
830
831 /// Build a reverse diff from current dirty state.
832 fn build_reverse_diff(&self) -> CommitDiff<N> {
833 let mut changes = BTreeMap::new();
834 self.capture_modified_chunks(&mut changes);
835 self.capture_appended_chunks(&mut changes);
836 self.capture_popped_chunks(&mut changes);
837 self.capture_pruned_chunks(&mut changes);
838 CommitDiff {
839 len: self.state.base_len,
840 pruned_chunks: self.state.base_pruned_chunks,
841 chunk_diffs: changes,
842 }
843 }
844
845 /// Capture chunks affected by bit modifications.
846 ///
847 /// For each chunk containing modified bits, we store its original value so we can
848 /// restore it when reconstructing historical states.
849 fn capture_modified_chunks(&self, changes: &mut BTreeMap<usize, ChunkDiff<N>>) {
850 for &bit in self.state.modified_bits.keys() {
851 let chunk_idx = Prunable::<N>::unpruned_chunk(bit);
852 changes.entry(chunk_idx).or_insert_with(|| {
853 // `modified_bits` only contains bits from the base region, so the chunk must exist.
854 let old_chunk = self
855 .get_chunk_from_current(chunk_idx)
856 .expect("chunk must exist for modified bit");
857 ChunkDiff::Modified(old_chunk)
858 });
859 }
860 }
861
862 /// Capture chunks affected by appended bits.
863 ///
864 /// When bits are appended, they may:
865 /// - Extend an existing partial chunk (mark as Modified with old data)
866 /// - Create entirely new chunks (mark as Added)
867 fn capture_appended_chunks(&self, changes: &mut BTreeMap<usize, ChunkDiff<N>>) {
868 if self.state.appended_bits.is_empty() {
869 return;
870 }
871
872 // Calculate which chunks will be affected by appends.
873 // Note: append_start_bit accounts for any net pops before the pushes.
874 let append_start_bit = self.state.projected_len - self.state.appended_bits.len() as u64;
875 let start_chunk = Prunable::<N>::unpruned_chunk(append_start_bit);
876 let end_chunk = Prunable::<N>::unpruned_chunk(self.state.projected_len.saturating_sub(1));
877
878 for chunk_idx in start_chunk..=end_chunk {
879 // Use or_insert_with so we don't overwrite chunks already captured
880 // by capture_modified_chunks (which runs first and takes precedence).
881 changes.entry(chunk_idx).or_insert_with(|| {
882 self.get_chunk_from_current(chunk_idx).map_or(
883 // Chunk is brand new: mark as Added
884 ChunkDiff::Added,
885 // Chunk existed before: store its old data
886 ChunkDiff::Modified,
887 )
888 });
889 }
890 }
891
892 /// Capture chunks affected by pop operations.
893 ///
894 /// When bits are popped (projected_len < base_len), we need to capture the original
895 /// data of chunks that will be truncated or fully removed. This allows reconstruction
896 /// to restore the bits that were popped.
897 fn capture_popped_chunks(&self, changes: &mut BTreeMap<usize, ChunkDiff<N>>) {
898 if self.state.projected_len >= self.state.base_len || self.state.base_len == 0 {
899 return; // No net pops
900 }
901
902 // Identify the range of chunks affected by length reduction.
903 let old_last_chunk = Prunable::<N>::unpruned_chunk(self.state.base_len - 1);
904 let new_last_chunk = if self.state.projected_len > 0 {
905 Prunable::<N>::unpruned_chunk(self.state.projected_len - 1)
906 } else {
907 0
908 };
909
910 // Capture all chunks between the new and old endpoints.
911
912 // Handle the case where we popped all unpruned bits, leaving new_last_chunk
913 // < self.state.base_pruned_chunks. For example, suppose bitmap has 10 bits per chunk,
914 // and 50 entries, where 40 are pruned. Then we pop 10 bits to make the bitmap have 40 entries,
915 // where all 40 are pruned. Then new_last_chunk is 3 and self.state.base_pruned_chunks is 4.
916 let start_chunk = self.state.base_pruned_chunks.max(new_last_chunk);
917
918 for chunk_idx in start_chunk..=old_last_chunk {
919 changes.entry(chunk_idx).or_insert_with(|| {
920 let old_chunk = self
921 .get_chunk_from_current(chunk_idx)
922 .expect("chunk must exist in base bitmap for popped bits");
923
924 // Determine if this chunk is partially kept or completely removed
925 let chunk_start_bit = chunk_idx as u64 * Prunable::<N>::CHUNK_SIZE_BITS;
926
927 if self.state.projected_len > chunk_start_bit {
928 // Chunk spans the new length boundary → partially kept (Modified)
929 ChunkDiff::Modified(old_chunk)
930 } else {
931 // Chunk is completely beyond the new length → fully removed (Removed)
932 ChunkDiff::Removed(old_chunk)
933 }
934 });
935 }
936 }
937
938 /// Capture chunks that will be pruned.
939 ///
940 /// The `prune_to_bit` method already captured the old chunk data,
941 /// so we simply copy it into the reverse diff.
942 fn capture_pruned_chunks(&self, changes: &mut BTreeMap<usize, ChunkDiff<N>>) {
943 for (&chunk_idx, &chunk_data) in &self.state.chunks_to_prune {
944 changes.insert(chunk_idx, ChunkDiff::Pruned(chunk_data));
945 }
946 }
947
948 /// Get chunk data from current state if it exists.
949 ///
950 /// Returns `Some(chunk_data)` if the chunk exists in the current bitmap,
951 /// or `None` if it's out of bounds or pruned.
952 fn get_chunk_from_current(&self, chunk_idx: usize) -> Option<[u8; N]> {
953 let current_pruned = self.current.pruned_chunks();
954 if chunk_idx >= current_pruned {
955 let bitmap_idx = chunk_idx - current_pruned;
956 if bitmap_idx < self.current.chunks_len() {
957 return Some(*self.current.get_chunk(bitmap_idx));
958 }
959 }
960 None
961 }
962}