Skip to main content

reth_trie/
forward_cursor.rs

1/// The implementation of forward-only in memory cursor over the entries.
2///
3/// The cursor operates under the assumption that the supplied collection is pre-sorted.
4#[derive(Debug)]
5pub struct ForwardInMemoryCursor<'a, K, V> {
6    /// The reference to the pre-sorted collection of entries.
7    entries: &'a [(K, V)],
8    /// Current index in the collection.
9    idx: usize,
10}
11
12impl<'a, K, V> ForwardInMemoryCursor<'a, K, V> {
13    /// Create new forward cursor positioned at the beginning of the collection.
14    ///
15    /// The cursor expects all of the entries to have been sorted in advance.
16    #[inline]
17    pub const fn new(entries: &'a [(K, V)]) -> Self {
18        Self { entries, idx: 0 }
19    }
20
21    /// Returns `true` if the cursor is empty, regardless of its position.
22    #[inline]
23    pub const fn is_empty(&self) -> bool {
24        self.entries.is_empty()
25    }
26
27    /// Returns `true` if any entry satisfies the predicate.
28    #[inline]
29    pub fn has_any<F>(&self, predicate: F) -> bool
30    where
31        F: Fn(&(K, V)) -> bool,
32    {
33        self.entries.iter().any(predicate)
34    }
35
36    /// Returns the current entry pointed to be the cursor, or `None` if no entries are left.
37    #[inline]
38    pub fn current(&self) -> Option<&(K, V)> {
39        self.entries.get(self.idx)
40    }
41
42    /// Resets the cursor to the beginning of the collection.
43    #[inline]
44    pub const fn reset(&mut self) {
45        self.idx = 0;
46    }
47
48    #[inline]
49    fn next(&mut self) -> Option<&(K, V)> {
50        let entry = self.entries.get(self.idx)?;
51        self.idx += 1;
52        Some(entry)
53    }
54}
55
56/// Threshold for remaining entries above which binary search is used instead of linear scan.
57/// For small slices, linear scan has better cache locality and lower overhead.
58const BINARY_SEARCH_THRESHOLD: usize = 64;
59
60impl<K: Ord, V> ForwardInMemoryCursor<'_, K, V> {
61    /// Returns the first entry from the current cursor position that's greater or equal to the
62    /// provided key. This method advances the cursor forward.
63    pub fn seek(&mut self, key: &K) -> Option<&(K, V)> {
64        self.advance_while(|k| k < key)
65    }
66
67    /// Returns the first entry from the current cursor position that's greater than the provided
68    /// key. This method advances the cursor forward.
69    pub fn first_after(&mut self, key: &K) -> Option<&(K, V)> {
70        self.advance_while(|k| k <= key)
71    }
72
73    /// Advances the cursor forward while `predicate` returns `true` or until the collection is
74    /// exhausted.
75    ///
76    /// Uses binary search for large remaining slices (>= 64 entries), linear scan for small ones.
77    ///
78    /// Returns the first entry for which `predicate` returns `false` or `None`. The cursor will
79    /// point to the returned entry.
80    fn advance_while(&mut self, predicate: impl Fn(&K) -> bool) -> Option<&(K, V)> {
81        let remaining = self.entries.len().saturating_sub(self.idx);
82        if remaining >= BINARY_SEARCH_THRESHOLD {
83            let slice = &self.entries[self.idx..];
84            let pos = slice.partition_point(|(k, _)| predicate(k));
85            self.idx += pos;
86        } else {
87            while self.current().is_some_and(|(k, _)| predicate(k)) {
88                self.next();
89            }
90        }
91        self.current()
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn test_cursor_small() {
101        let mut cursor = ForwardInMemoryCursor::new(&[(1, ()), (2, ()), (3, ()), (4, ()), (5, ())]);
102        assert_eq!(cursor.current(), Some(&(1, ())));
103
104        assert_eq!(cursor.seek(&0), Some(&(1, ())));
105        assert_eq!(cursor.current(), Some(&(1, ())));
106
107        assert_eq!(cursor.seek(&3), Some(&(3, ())));
108        assert_eq!(cursor.current(), Some(&(3, ())));
109
110        assert_eq!(cursor.seek(&3), Some(&(3, ())));
111        assert_eq!(cursor.current(), Some(&(3, ())));
112
113        assert_eq!(cursor.seek(&4), Some(&(4, ())));
114        assert_eq!(cursor.current(), Some(&(4, ())));
115
116        assert_eq!(cursor.seek(&6), None);
117        assert_eq!(cursor.current(), None);
118    }
119
120    #[test]
121    fn test_cursor_large_binary_search() {
122        // Create a large enough collection to trigger binary search
123        let entries: Vec<(i32, ())> = (0..200).map(|i| (i * 2, ())).collect();
124        let mut cursor = ForwardInMemoryCursor::new(&entries);
125
126        // Seek to beginning
127        assert_eq!(cursor.seek(&0), Some(&(0, ())));
128        assert_eq!(cursor.idx, 0);
129
130        // Seek to middle (should use binary search)
131        assert_eq!(cursor.seek(&100), Some(&(100, ())));
132        assert_eq!(cursor.idx, 50);
133
134        // Seek to non-existent key (should find next greater)
135        assert_eq!(cursor.seek(&101), Some(&(102, ())));
136        assert_eq!(cursor.idx, 51);
137
138        // Seek to end
139        assert_eq!(cursor.seek(&398), Some(&(398, ())));
140        assert_eq!(cursor.idx, 199);
141
142        // Seek past end
143        assert_eq!(cursor.seek(&1000), None);
144    }
145
146    #[test]
147    fn test_first_after_large() {
148        let entries: Vec<(i32, ())> = (0..200).map(|i| (i * 2, ())).collect();
149        let mut cursor = ForwardInMemoryCursor::new(&entries);
150
151        // first_after should find strictly greater
152        assert_eq!(cursor.first_after(&0), Some(&(2, ())));
153        assert_eq!(cursor.idx, 1);
154
155        // Reset and test from beginning
156        cursor.reset();
157        assert_eq!(cursor.first_after(&99), Some(&(100, ())));
158
159        // first_after on exact match
160        cursor.reset();
161        assert_eq!(cursor.first_after(&100), Some(&(102, ())));
162    }
163
164    #[test]
165    fn test_cursor_consistency() {
166        // Verify binary search and linear scan produce same results
167        let entries: Vec<(i32, ())> = (0..200).map(|i| (i * 3, ())).collect();
168
169        for search_key in [0, 1, 3, 50, 150, 299, 300, 597, 598, 599, 1000] {
170            // Test with fresh cursor (binary search path)
171            let mut cursor1 = ForwardInMemoryCursor::new(&entries);
172            let result1 = cursor1.seek(&search_key);
173
174            // Manually advance to trigger linear path by getting close first
175            let mut cursor2 = ForwardInMemoryCursor::new(&entries);
176            if search_key > 100 {
177                cursor2.seek(&(search_key - 50));
178            }
179            let result2 = cursor2.seek(&search_key);
180
181            assert_eq!(
182                result1, result2,
183                "Mismatch for key {search_key}: binary={result1:?}, linear={result2:?}"
184            );
185        }
186    }
187}