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: ptr points to COVERAGE_MAP_SIZE bytes, byte < COVERAGE_MAP_SIZE
36        unsafe {
37            *self.ptr.add(byte) |= 1 << bit;
38        }
39    }
40
41    /// Clear all bits to zero.
42    pub fn clear(&self) {
43        // Safety: ptr points to COVERAGE_MAP_SIZE bytes
44        unsafe {
45            std::ptr::write_bytes(self.ptr, 0, COVERAGE_MAP_SIZE);
46        }
47    }
48
49    /// Get a pointer to the underlying data.
50    pub fn as_ptr(&self) -> *const u8 {
51        self.ptr
52    }
53}
54
55/// Cross-process coverage map, OR'd by all timelines.
56///
57/// Lives in `MAP_SHARED` memory so all forked timelines contribute.
58/// A bit set to 1 means "this assertion path has been explored."
59pub struct ExploredMap {
60    ptr: *mut u8,
61}
62
63impl ExploredMap {
64    /// Wrap a shared-memory pointer as a explored map.
65    ///
66    /// # Safety
67    ///
68    /// `ptr` must point to at least [`COVERAGE_MAP_SIZE`] bytes of valid,
69    /// writable shared memory.
70    pub unsafe fn new(ptr: *mut u8) -> Self {
71        Self { ptr }
72    }
73
74    /// Merge a timeline's coverage bitmap into this explored map (bitwise OR).
75    pub fn merge_from(&self, other: &CoverageBitmap) {
76        // Safety: both pointers are valid for COVERAGE_MAP_SIZE bytes
77        unsafe {
78            for i in 0..COVERAGE_MAP_SIZE {
79                *self.ptr.add(i) |= *other.as_ptr().add(i);
80            }
81        }
82    }
83
84    /// Count the number of set bits in the explored map (population count).
85    ///
86    /// Returns the total number of unique assertion paths explored across
87    /// all timelines.
88    pub fn count_bits_set(&self) -> u32 {
89        let mut count: u32 = 0;
90        // Safety: ptr points to COVERAGE_MAP_SIZE bytes
91        unsafe {
92            for i in 0..COVERAGE_MAP_SIZE {
93                count += (*self.ptr.add(i)).count_ones();
94            }
95        }
96        count
97    }
98
99    /// Check if a timeline's bitmap contains any bits not yet in the explored map.
100    pub fn has_new_bits(&self, other: &CoverageBitmap) -> bool {
101        // Safety: both pointers are valid for COVERAGE_MAP_SIZE bytes
102        unsafe {
103            for i in 0..COVERAGE_MAP_SIZE {
104                let explored = *self.ptr.add(i);
105                let child = *other.as_ptr().add(i);
106                // Child has bits that explored map doesn't
107                if (child & !explored) != 0 {
108                    return true;
109                }
110            }
111        }
112        false
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use crate::shared_mem;
120
121    #[test]
122    fn test_set_bit_and_check() {
123        let ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
124        let explored_ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
125        let bm = unsafe { CoverageBitmap::new(ptr) };
126        let vm = unsafe { ExploredMap::new(explored_ptr) };
127
128        // Initially no new bits
129        assert!(!vm.has_new_bits(&bm));
130
131        // Set a bit in bitmap
132        bm.set_bit(42);
133        assert!(vm.has_new_bits(&bm));
134
135        // Merge into explored map
136        vm.merge_from(&bm);
137        // Now no new bits (already merged)
138        assert!(!vm.has_new_bits(&bm));
139
140        unsafe {
141            shared_mem::free_shared(ptr, COVERAGE_MAP_SIZE);
142            shared_mem::free_shared(explored_ptr, COVERAGE_MAP_SIZE);
143        }
144    }
145
146    #[test]
147    fn test_clear() {
148        let ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
149        let bm = unsafe { CoverageBitmap::new(ptr) };
150
151        bm.set_bit(0);
152        bm.set_bit(100);
153        bm.set_bit(8000);
154
155        bm.clear();
156
157        // Verify all zeroed
158        unsafe {
159            for i in 0..COVERAGE_MAP_SIZE {
160                assert_eq!(*ptr.add(i), 0);
161            }
162            shared_mem::free_shared(ptr, COVERAGE_MAP_SIZE);
163        }
164    }
165
166    #[test]
167    fn test_merge_accumulates() {
168        let bm1_ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
169        let bm2_ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
170        let vm_ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
171
172        let bm1 = unsafe { CoverageBitmap::new(bm1_ptr) };
173        let bm2 = unsafe { CoverageBitmap::new(bm2_ptr) };
174        let vm = unsafe { ExploredMap::new(vm_ptr) };
175
176        bm1.set_bit(10);
177        bm2.set_bit(20);
178
179        vm.merge_from(&bm1);
180        // bm2 has bit 20 which is new
181        assert!(vm.has_new_bits(&bm2));
182
183        vm.merge_from(&bm2);
184        // Now neither has new bits
185        assert!(!vm.has_new_bits(&bm1));
186        assert!(!vm.has_new_bits(&bm2));
187
188        unsafe {
189            shared_mem::free_shared(bm1_ptr, COVERAGE_MAP_SIZE);
190            shared_mem::free_shared(bm2_ptr, COVERAGE_MAP_SIZE);
191            shared_mem::free_shared(vm_ptr, COVERAGE_MAP_SIZE);
192        }
193    }
194
195    #[test]
196    fn test_count_bits_set() {
197        let vm_ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
198        let bm_ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
199        let vm = unsafe { ExploredMap::new(vm_ptr) };
200        let bm = unsafe { CoverageBitmap::new(bm_ptr) };
201
202        assert_eq!(vm.count_bits_set(), 0);
203
204        bm.set_bit(0);
205        bm.set_bit(42);
206        bm.set_bit(8000);
207        vm.merge_from(&bm);
208
209        assert_eq!(vm.count_bits_set(), 3);
210
211        unsafe {
212            shared_mem::free_shared(vm_ptr, COVERAGE_MAP_SIZE);
213            shared_mem::free_shared(bm_ptr, COVERAGE_MAP_SIZE);
214        }
215    }
216}