Skip to main content

moonpool_explorer/
coverage.rs

1//! Coverage tracking for exploration novelty detection.
2//!
3//! Tracks which assertion paths have been explored across all timelines.
4//! The [`ExploredMap`] lives in shared memory and accumulates coverage
5//! from every timeline. Each timeline gets a fresh [`CoverageBitmap`]
6//! that is merged into the explored map after the timeline finishes.
7
8/// Size of coverage bitmaps in bytes (8192 bit positions).
9pub const COVERAGE_MAP_SIZE: usize = 1024;
10
11/// Per-timeline coverage bitmap, cleared before each split.
12///
13/// Tracks which assertion paths were hit during this timeline's execution.
14/// After the timeline finishes, the parent merges this into the [`ExploredMap`].
15pub struct CoverageBitmap {
16    ptr: *mut u8,
17}
18
19impl CoverageBitmap {
20    /// Wrap a shared-memory pointer as a coverage bitmap.
21    ///
22    /// # Safety
23    ///
24    /// `ptr` must point to at least [`COVERAGE_MAP_SIZE`] bytes of valid,
25    /// writable shared memory.
26    pub unsafe fn new(ptr: *mut u8) -> Self {
27        Self { ptr }
28    }
29
30    /// Set the bit at the given index (mod total bits).
31    pub fn set_bit(&self, index: usize) {
32        let bit_index = index % (COVERAGE_MAP_SIZE * 8);
33        let byte = bit_index / 8;
34        let bit = bit_index % 8;
35        // Safety: self.ptr points to COVERAGE_MAP_SIZE bytes (constructor invariant).
36        // The modular arithmetic (bit_index % (COVERAGE_MAP_SIZE * 8)) guarantees
37        // byte = bit_index / 8 < COVERAGE_MAP_SIZE, so ptr.add(byte) is in bounds.
38        unsafe {
39            *self.ptr.add(byte) |= 1 << bit;
40        }
41    }
42
43    /// Clear all bits to zero.
44    pub fn clear(&self) {
45        // Safety: self.ptr points to COVERAGE_MAP_SIZE bytes of writable shared
46        // memory (constructor invariant). write_bytes zeroes exactly that region.
47        unsafe {
48            std::ptr::write_bytes(self.ptr, 0, COVERAGE_MAP_SIZE);
49        }
50    }
51
52    /// Get a pointer to the underlying data.
53    #[must_use]
54    pub fn as_ptr(&self) -> *const u8 {
55        self.ptr
56    }
57}
58
59/// Cross-process coverage map, OR'd by all timelines.
60///
61/// Lives in `MAP_SHARED` memory so all forked timelines contribute.
62/// A bit set to 1 means "this assertion path has been explored."
63pub struct ExploredMap {
64    ptr: *mut u8,
65}
66
67impl ExploredMap {
68    /// Wrap a shared-memory pointer as a explored map.
69    ///
70    /// # Safety
71    ///
72    /// `ptr` must point to at least [`COVERAGE_MAP_SIZE`] bytes of valid,
73    /// writable shared memory.
74    pub unsafe fn new(ptr: *mut u8) -> Self {
75        Self { ptr }
76    }
77
78    /// Merge a timeline's coverage bitmap into this explored map (bitwise OR).
79    pub fn merge_from(&self, other: &CoverageBitmap) {
80        // Safety: both pointers are valid for COVERAGE_MAP_SIZE bytes
81        // (constructor invariants of ExploredMap and CoverageBitmap).
82        let explored = unsafe { std::slice::from_raw_parts_mut(self.ptr, COVERAGE_MAP_SIZE) };
83        let child = unsafe { std::slice::from_raw_parts(other.as_ptr(), COVERAGE_MAP_SIZE) };
84        for (e, c) in explored.iter_mut().zip(child) {
85            *e |= *c;
86        }
87    }
88
89    /// Count the number of set bits in the explored map (population count).
90    ///
91    /// Returns the total number of unique assertion paths explored across
92    /// all timelines.
93    #[must_use]
94    pub fn count_bits_set(&self) -> u32 {
95        // Safety: self.ptr points to COVERAGE_MAP_SIZE bytes (constructor invariant).
96        let explored = unsafe { std::slice::from_raw_parts(self.ptr, COVERAGE_MAP_SIZE) };
97        explored.iter().map(|b| b.count_ones()).sum()
98    }
99
100    /// Check if a timeline's bitmap contains any bits not yet in the explored map.
101    #[must_use]
102    pub fn has_new_bits(&self, other: &CoverageBitmap) -> bool {
103        // Safety: both pointers are valid for COVERAGE_MAP_SIZE bytes
104        // (constructor invariants of ExploredMap and CoverageBitmap).
105        let explored = unsafe { std::slice::from_raw_parts(self.ptr, COVERAGE_MAP_SIZE) };
106        let child = unsafe { std::slice::from_raw_parts(other.as_ptr(), COVERAGE_MAP_SIZE) };
107        explored.iter().zip(child).any(|(e, c)| (c & !e) != 0)
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::shared_mem;
115
116    #[test]
117    fn test_set_bit_and_check() {
118        let ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
119        let explored_ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
120        let bm = unsafe { CoverageBitmap::new(ptr) };
121        let vm = unsafe { ExploredMap::new(explored_ptr) };
122
123        // Initially no new bits
124        assert!(!vm.has_new_bits(&bm));
125
126        // Set a bit in bitmap
127        bm.set_bit(42);
128        assert!(vm.has_new_bits(&bm));
129
130        // Merge into explored map
131        vm.merge_from(&bm);
132        // Now no new bits (already merged)
133        assert!(!vm.has_new_bits(&bm));
134
135        unsafe {
136            shared_mem::free_shared(ptr, COVERAGE_MAP_SIZE);
137            shared_mem::free_shared(explored_ptr, COVERAGE_MAP_SIZE);
138        }
139    }
140
141    #[test]
142    fn test_clear() {
143        let ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
144        let bm = unsafe { CoverageBitmap::new(ptr) };
145
146        bm.set_bit(0);
147        bm.set_bit(100);
148        bm.set_bit(8000);
149
150        bm.clear();
151
152        // Verify all zeroed
153        unsafe {
154            for i in 0..COVERAGE_MAP_SIZE {
155                assert_eq!(*ptr.add(i), 0);
156            }
157            shared_mem::free_shared(ptr, COVERAGE_MAP_SIZE);
158        }
159    }
160
161    #[test]
162    fn test_merge_accumulates() {
163        let bm1_ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
164        let bm2_ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
165        let vm_ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
166
167        let bm1 = unsafe { CoverageBitmap::new(bm1_ptr) };
168        let bm2 = unsafe { CoverageBitmap::new(bm2_ptr) };
169        let vm = unsafe { ExploredMap::new(vm_ptr) };
170
171        bm1.set_bit(10);
172        bm2.set_bit(20);
173
174        vm.merge_from(&bm1);
175        // bm2 has bit 20 which is new
176        assert!(vm.has_new_bits(&bm2));
177
178        vm.merge_from(&bm2);
179        // Now neither has new bits
180        assert!(!vm.has_new_bits(&bm1));
181        assert!(!vm.has_new_bits(&bm2));
182
183        unsafe {
184            shared_mem::free_shared(bm1_ptr, COVERAGE_MAP_SIZE);
185            shared_mem::free_shared(bm2_ptr, COVERAGE_MAP_SIZE);
186            shared_mem::free_shared(vm_ptr, COVERAGE_MAP_SIZE);
187        }
188    }
189
190    #[test]
191    fn test_count_bits_set() {
192        let vm_ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
193        let bm_ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
194        let vm = unsafe { ExploredMap::new(vm_ptr) };
195        let bm = unsafe { CoverageBitmap::new(bm_ptr) };
196
197        assert_eq!(vm.count_bits_set(), 0);
198
199        bm.set_bit(0);
200        bm.set_bit(42);
201        bm.set_bit(8000);
202        vm.merge_from(&bm);
203
204        assert_eq!(vm.count_bits_set(), 3);
205
206        unsafe {
207            shared_mem::free_shared(vm_ptr, COVERAGE_MAP_SIZE);
208            shared_mem::free_shared(bm_ptr, COVERAGE_MAP_SIZE);
209        }
210    }
211}