Skip to main content

perl_lexer/checkpoint/
cache.rs

1use crate::checkpoint::LexerCheckpoint;
2
3/// A checkpoint cache for efficient incremental parsing
4pub struct CheckpointCache {
5    /// Cached checkpoints at various positions
6    checkpoints: Vec<(usize, LexerCheckpoint)>,
7    /// Maximum number of checkpoints to cache
8    max_checkpoints: usize,
9}
10
11impl CheckpointCache {
12    /// Create a new checkpoint cache.
13    pub fn new(max_checkpoints: usize) -> Self {
14        Self { checkpoints: Vec::new(), max_checkpoints }
15    }
16
17    /// Number of checkpoints currently held in the cache.
18    pub fn len(&self) -> usize {
19        self.checkpoints.len()
20    }
21
22    /// Whether the cache currently holds zero checkpoints.
23    pub fn is_empty(&self) -> bool {
24        self.checkpoints.is_empty()
25    }
26
27    /// Add a checkpoint to the cache.
28    ///
29    /// If a checkpoint already exists at the same byte position, it is
30    /// replaced in place. Otherwise the new checkpoint is inserted at the
31    /// correct sorted index so [`Self::find_before`] and [`Self::find_after`]
32    /// can use binary search.
33    ///
34    /// When the cache exceeds `max_checkpoints`, entries are evicted while
35    /// preserving the earliest and latest checkpoints as boundary anchors for
36    /// incremental parsing windows.
37    pub fn add(&mut self, checkpoint: LexerCheckpoint) {
38        if self.max_checkpoints == 0 {
39            return;
40        }
41
42        let position = checkpoint.position;
43        match self.checkpoints.binary_search_by_key(&position, |(pos, _)| *pos) {
44            Ok(idx) => self.checkpoints[idx] = (position, checkpoint),
45            Err(idx) => self.checkpoints.insert(idx, (position, checkpoint)),
46        }
47
48        if self.checkpoints.len() > self.max_checkpoints {
49            let total = self.checkpoints.len();
50            if self.max_checkpoints == 1 {
51                if let Some(last) = self.checkpoints.last().cloned() {
52                    self.checkpoints = vec![last];
53                }
54                return;
55            }
56
57            let denominator = self.max_checkpoints - 1;
58            let mut kept = Vec::with_capacity(self.max_checkpoints);
59            for i in 0..self.max_checkpoints {
60                let idx = i * (total - 1) / denominator;
61                kept.push(self.checkpoints[idx].clone());
62            }
63            self.checkpoints = kept;
64        }
65    }
66
67    /// Find the nearest checkpoint at or before a given position.
68    ///
69    /// Uses binary search over the sorted `checkpoints` vector (invariant
70    /// maintained by [`Self::add`]) for O(log N) rather than the previous
71    /// O(N) linear scan. This matters when the checkpoint limit is large
72    /// (50+) for big documents (#2080).
73    pub fn find_before(&self, position: usize) -> Option<&LexerCheckpoint> {
74        // `partition_point` returns the index of the first element for which
75        // the predicate is false (i.e. the first entry with pos > position).
76        // The entry just before that index is the last one with pos <= position.
77        let idx = self.checkpoints.partition_point(|(pos, _)| *pos <= position);
78        if idx == 0 { None } else { self.checkpoints.get(idx - 1).map(|(_, cp)| cp) }
79    }
80
81    /// Find the nearest checkpoint at or after a given position.
82    ///
83    /// Uses binary search over the sorted `checkpoints` vector (invariant
84    /// maintained by [`Self::add`]) for O(log N) performance. This is the
85    /// counterpart to [`Self::find_before`] and is needed for two-sided
86    /// checkpoint windows in incremental parsing (#3527).
87    ///
88    /// # Arguments
89    ///
90    /// * `position` - The byte position to search from
91    ///
92    /// # Returns
93    ///
94    /// * `Some(&LexerCheckpoint)` - The checkpoint at or after the given position
95    /// * `None` - If no checkpoint exists at or after the position
96    ///
97    /// # Examples
98    ///
99    /// ```
100    /// # use perl_lexer::checkpoint::{CheckpointCache, LexerCheckpoint};
101    /// let mut cache = CheckpointCache::new(10);
102    /// cache.add(LexerCheckpoint::at_position(100));
103    /// cache.add(LexerCheckpoint::at_position(200));
104    /// cache.add(LexerCheckpoint::at_position(300));
105    ///
106    /// // Find checkpoint at or after position 150
107    /// let cp = cache.find_after(150);
108    /// assert!(matches!(cp, Some(found) if found.position == 200));
109    ///
110    /// // Find checkpoint at exact position
111    /// let cp = cache.find_after(200);
112    /// assert!(matches!(cp, Some(found) if found.position == 200));
113    ///
114    /// // Position beyond last checkpoint returns None
115    /// let cp = cache.find_after(400);
116    /// assert!(cp.is_none());
117    /// ```
118    pub fn find_after(&self, position: usize) -> Option<&LexerCheckpoint> {
119        // `partition_point` returns the index of the first element for which
120        // the predicate is false (i.e. the first entry with pos >= position).
121        let idx = self.checkpoints.partition_point(|(pos, _)| *pos < position);
122        self.checkpoints.get(idx).map(|(_, cp)| cp)
123    }
124
125    /// Clear all cached checkpoints.
126    pub fn clear(&mut self) {
127        self.checkpoints.clear();
128    }
129
130    /// Apply an edit to all cached checkpoints, shifting or invalidating each
131    /// entry and re-sorting to preserve the binary-search invariant.
132    pub fn apply_edit(&mut self, start: usize, old_len: usize, new_len: usize) {
133        for (pos, checkpoint) in &mut self.checkpoints {
134            checkpoint.apply_edit(start, old_len, new_len);
135            *pos = checkpoint.position;
136        }
137
138        // Edits can move checkpoints backward (or to the same position), so
139        // restore the sorted-order invariant required by binary-search lookups.
140        self.checkpoints.sort_unstable_by_key(|(pos, _)| *pos);
141    }
142}