Skip to main content

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}