@@ -634,6 +634,7 @@ where
634634 let bitmap_batch = BitmapBatch :: Layer ( Arc :: new ( BitmapBatchLayer {
635635 parent : bitmap_parent. clone ( ) ,
636636 overlay : Arc :: new ( overlay) ,
637+ shared : Arc :: clone ( bitmap_parent. shared ( ) ) ,
637638 } ) ) ;
638639
639640 // Compute canonical root. The grafted batch alone cannot resolve committed nodes,
@@ -763,19 +764,19 @@ pub(crate) struct BitmapBatchLayer<const N: usize> {
763764 pub ( crate ) parent : BitmapBatch < N > ,
764765 /// Chunk-level overlay: materialized bytes for every chunk that differs from parent.
765766 pub ( crate ) overlay : Arc < ChunkOverlay < N > > ,
767+ /// Cached terminal [`SharedBitmap`] so [`BitmapBatch::shared`] and
768+ /// [`BitmapBatch::pruned_chunks`] answer in O(1) instead of walking the chain.
769+ pub ( crate ) shared : Arc < SharedBitmap < N > > ,
766770}
767771
768772impl < const N : usize > BitmapBatch < N > {
769773 const CHUNK_SIZE_BITS : u64 = BitMap :: < N > :: CHUNK_SIZE_BITS ;
770774
771- /// Walk to the terminal [`SharedBitmap`] at the bottom of the chain.
775+ /// Return the terminal [`SharedBitmap`] at the bottom of the chain.
772776 fn shared ( & self ) -> & Arc < SharedBitmap < N > > {
773- let mut current = self ;
774- loop {
775- match current {
776- Self :: Base ( s) => return s,
777- Self :: Layer ( layer) => current = & layer. parent ,
778- }
777+ match self {
778+ Self :: Base ( s) => s,
779+ Self :: Layer ( layer) => & layer. shared ,
779780 }
780781 }
781782
@@ -799,6 +800,7 @@ impl<const N: usize> BitmapBatch<N> {
799800 result = Self :: Layer ( Arc :: new ( BitmapBatchLayer {
800801 parent : result,
801802 overlay,
803+ shared : Arc :: clone ( shared) ,
802804 } ) ) ;
803805 }
804806 result
@@ -843,13 +845,7 @@ impl<const N: usize> BitmapReadable<N> for BitmapBatch<N> {
843845 }
844846
845847 fn pruned_chunks ( & self ) -> usize {
846- let mut current = self ;
847- loop {
848- match current {
849- Self :: Base ( shared) => return shared. pruned_chunks ( ) ,
850- Self :: Layer ( layer) => current = & layer. parent ,
851- }
852- }
848+ self . shared ( ) . pruned_chunks ( )
853849 }
854850
855851 fn len ( & self ) -> u64 {
@@ -1380,4 +1376,124 @@ mod tests {
13801376 // Empty bitmap, tip = 0: no candidates.
13811377 assert_eq ! ( scan. next_candidate( Location :: new( 0 ) , 0 ) , None ) ;
13821378 }
1379+
1380+ // ---- trim_committed tests ----
1381+ //
1382+ // `trim_committed` is called from `MerkleizedBatch::new_batch` to strip any `Layer`s whose
1383+ // overlays have already been absorbed into the shared committed bitmap by a prior apply.
1384+ // The implementation is a single loop that collects uncommitted overlays top-down and
1385+ // rebuilds a fresh chain rooted at `Base`. These tests cover distinct input shapes directly,
1386+ // without going through the full Db/batch machinery, so the function's structural output
1387+ // can be asserted.
1388+
1389+ /// Build a chain `Base(shared) -> Layer(len=L1) -> Layer(len=L2) -> ...` from a list of
1390+ /// overlay lengths (bottom to top). Each constructed `Layer` caches `shared` per the
1391+ /// struct's invariant.
1392+ fn make_chain ( shared : & Arc < SharedBitmap < N > > , overlay_lens : & [ u64 ] ) -> BitmapBatch < N > {
1393+ let mut chain = BitmapBatch :: Base ( Arc :: clone ( shared) ) ;
1394+ for & len in overlay_lens {
1395+ chain = BitmapBatch :: Layer ( Arc :: new ( BitmapBatchLayer {
1396+ parent : chain,
1397+ overlay : Arc :: new ( ChunkOverlay :: new ( len) ) ,
1398+ shared : Arc :: clone ( shared) ,
1399+ } ) ) ;
1400+ }
1401+ chain
1402+ }
1403+
1404+ /// Walk a chain and return its overlay lengths in bottom-to-top order. Used to assert the
1405+ /// structural output of `trim_committed` without touching private fields. Panics if the
1406+ /// chain isn't terminated by a single `Base` at the bottom.
1407+ fn chain_overlays ( batch : & BitmapBatch < N > ) -> Vec < u64 > {
1408+ let mut lens = Vec :: new ( ) ;
1409+ let mut current = batch;
1410+ while let BitmapBatch :: Layer ( layer) = current {
1411+ lens. push ( layer. overlay . len ) ;
1412+ current = & layer. parent ;
1413+ }
1414+ assert ! ( matches!( current, BitmapBatch :: Base ( _) ) ) ;
1415+ lens. reverse ( ) ;
1416+ lens
1417+ }
1418+
1419+ /// Input is already a bare `Base` with no speculative layers on top — the loop body never
1420+ /// runs, `kept` stays empty, and the result is a freshly constructed `Base` pointing at the
1421+ /// same `SharedBitmap`. Real-world trigger: `MerkleizedBatch::new_batch` on a batch whose
1422+ /// chain was previously trimmed flat (e.g., immediately after an apply collapsed everything).
1423+ #[ test]
1424+ fn trim_committed_already_base ( ) {
1425+ let shared = Arc :: new ( SharedBitmap :: < N > :: new ( make_bitmap ( & [ true ; 64 ] ) ) ) ;
1426+ let base = BitmapBatch :: Base ( Arc :: clone ( & shared) ) ;
1427+ let result = base. trim_committed ( ) ;
1428+ // Still `Base`, pointing at the same shared terminal.
1429+ match result {
1430+ BitmapBatch :: Base ( s) => assert ! ( Arc :: ptr_eq( & s, & shared) ) ,
1431+ BitmapBatch :: Layer ( _) => panic ! ( "expected Base" ) ,
1432+ }
1433+ }
1434+
1435+ /// Every layer has been absorbed by prior applies — the loop breaks on the first iteration
1436+ /// and `kept` stays empty, so the result is a bare `Base`. This is the steady-state
1437+ /// "extend a just-applied batch" flow: after `apply_batch(A)`, `A`'s own layer has
1438+ /// `overlay.len == committed` and the next `new_batch` call should start from a clean
1439+ /// terminal.
1440+ #[ test]
1441+ fn trim_committed_all_committed ( ) {
1442+ // `shared.len() == 64`; the single layer's `overlay.len == 32 (<= 64)`, so it's committed.
1443+ let shared = Arc :: new ( SharedBitmap :: < N > :: new ( make_bitmap ( & [ true ; 64 ] ) ) ) ;
1444+ let chain = make_chain ( & shared, & [ 32 ] ) ;
1445+ let result = chain. trim_committed ( ) ;
1446+ // Collapsed to a bare Base, pointing at the original shared.
1447+ match result {
1448+ BitmapBatch :: Base ( s) => assert ! ( Arc :: ptr_eq( & s, & shared) ) ,
1449+ BitmapBatch :: Layer ( _) => panic ! ( "expected Base after full trim" ) ,
1450+ }
1451+ }
1452+
1453+ /// Every layer is still speculative — the loop walks all the way to `Base` without
1454+ /// breaking, and `kept` holds every overlay. The rebuilt chain is structurally equivalent
1455+ /// to the input (same overlay lens, same shared terminal). Real-world trigger: speculating
1456+ /// multiple batches deep (A, then B off A, then C off B) without `apply_batch` in between.
1457+ #[ test]
1458+ fn trim_committed_none_committed ( ) {
1459+ // `shared.len() == 32`; both overlays have `len > 32`, so neither is committed.
1460+ let shared = Arc :: new ( SharedBitmap :: < N > :: new ( make_bitmap ( & [ true ; 32 ] ) ) ) ;
1461+ let chain = make_chain ( & shared, & [ 64 , 96 ] ) ;
1462+ let result = chain. trim_committed ( ) ;
1463+ // Structure must be preserved in bottom-to-top order.
1464+ assert_eq ! ( chain_overlays( & result) , vec![ 64 , 96 ] ) ;
1465+ }
1466+
1467+ /// Exactly one layer is uncommitted (the newest) on top of a committed prefix — the
1468+ /// dominant pattern in chained growth. The loop collects the one uncommitted overlay, and
1469+ /// the rebuild produces `Layer(Base, overlay_B)`. Also verifies the rebuilt layer carries
1470+ /// the cached `shared` reference correctly. Real-world trigger: apply parent A, then B
1471+ /// held alive off A, then `B.new_batch()` to build C.
1472+ #[ test]
1473+ fn trim_committed_exactly_one_uncommitted ( ) {
1474+ // `shared.len() == 64`; committed layer (`overlay.len == 64`) + uncommitted (`96`).
1475+ let shared = Arc :: new ( SharedBitmap :: < N > :: new ( make_bitmap ( & [ true ; 64 ] ) ) ) ;
1476+ let chain = make_chain ( & shared, & [ 64 , 96 ] ) ;
1477+ let result = chain. trim_committed ( ) ;
1478+ // The committed layer is gone; only the uncommitted overlay remains.
1479+ assert_eq ! ( chain_overlays( & result) , vec![ 96 ] ) ;
1480+ // And the rebuilt layer's `shared` field still points at the original terminal.
1481+ assert ! ( Arc :: ptr_eq( result. shared( ) , & shared) ) ;
1482+ }
1483+
1484+ /// Two or more uncommitted layers on top of a committed prefix — exercises the loop's
1485+ /// iterated `kept.push` and the rebuild's iterated `Arc::new(BitmapBatchLayer)`, including
1486+ /// the cached `shared` wire-through on every reconstructed layer. Real-world trigger:
1487+ /// build A, then B off A, then C off B; apply only A; then call `C.new_batch()`.
1488+ #[ test]
1489+ fn trim_committed_multiple_uncommitted ( ) {
1490+ // `shared.len() == 64`; committed layer (64), then two uncommitted (96, 128).
1491+ let shared = Arc :: new ( SharedBitmap :: < N > :: new ( make_bitmap ( & [ true ; 64 ] ) ) ) ;
1492+ let chain = make_chain ( & shared, & [ 64 , 96 , 128 ] ) ;
1493+ let result = chain. trim_committed ( ) ;
1494+ // Committed layer dropped; uncommitted pair preserved in order.
1495+ assert_eq ! ( chain_overlays( & result) , vec![ 96 , 128 ] ) ;
1496+ // Every reconstructed layer must still cache the original shared terminal.
1497+ assert ! ( Arc :: ptr_eq( result. shared( ) , & shared) ) ;
1498+ }
13831499}
0 commit comments