Skip to main content

sim_lib_sequence/mutable/
sparse.rs

1/// A failed sparse-sequence growth or length mutation.
2#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3pub enum SparseSequenceError {
4    /// The requested logical length exceeds the configured limit.
5    LengthLimit {
6        /// The requested logical length.
7        requested: usize,
8        /// The greatest permitted logical length.
9        limit: usize,
10    },
11    /// An index could not be converted into the required logical length.
12    IndexOverflow {
13        /// The index that could not be represented as `index + 1`.
14        index: usize,
15    },
16}
17impl fmt::Display for SparseSequenceError {
18    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
19        match self {
20            Self::LengthLimit { requested, limit } => {
21                write!(
22                    formatter,
23                    "sparse sequence length {requested} exceeds limit {limit}"
24                )
25            }
26            Self::IndexOverflow { index } => {
27                write!(
28                    formatter,
29                    "sparse sequence index {index} cannot grow the length"
30                )
31            }
32        }
33    }
34}
35
36impl std::error::Error for SparseSequenceError {}
37
38/// Mutable sparse indexed storage with stable holes and bounded allocation.
39///
40/// Logical length is stored separately from values. Writing a distant index
41/// allocates only its fixed-size chunk; intervening indices remain holes.
42/// `max_len` is an explicit work limit applied to every operation that can grow
43/// the logical sequence.
44#[derive(Clone, Debug)]
45pub struct SparseSequence<T> {
46    chunks: BTreeMap<usize, Box<[Option<T>; CHUNK_LEN]>>,
47    len: usize,
48    occupied: usize,
49    max_len: usize,
50    revision: u64,
51}
52
53impl<T: PartialEq> PartialEq for SparseSequence<T> {
54    fn eq(&self, other: &Self) -> bool {
55        self.len == other.len
56            && self.occupied == other.occupied
57            && self.occupied_in(..).eq(other.occupied_in(..))
58    }
59}
60
61impl<T: Eq> Eq for SparseSequence<T> {}
62
63impl<T> SparseSequence<T> {
64    /// Construct an empty store whose logical length may not exceed `max_len`.
65    pub fn new(max_len: usize) -> Self {
66        Self {
67            chunks: BTreeMap::new(),
68            len: 0,
69            occupied: 0,
70            max_len,
71            revision: 0,
72        }
73    }
74
75    /// Return the logical length, including holes.
76    pub fn len(&self) -> usize {
77        self.len
78    }
79
80    /// Return whether the logical sequence is empty.
81    pub fn is_empty(&self) -> bool {
82        self.len == 0
83    }
84
85    /// Return the configured logical-length limit.
86    pub fn max_len(&self) -> usize {
87        self.max_len
88    }
89
90    /// Return the number of occupied positions.
91    pub fn occupied_len(&self) -> usize {
92        self.occupied
93    }
94
95    /// Return the mutation revision.
96    ///
97    /// It advances for every successful operation that changes length or an
98    /// occupied position, using wrapping arithmetic so mutation never fails
99    /// merely because the diagnostic counter reached its integer limit.
100    pub fn revision(&self) -> u64 {
101        self.revision
102    }
103
104    /// Return the value at `index`, or `None` for a hole or out-of-range index.
105    pub fn get(&self, index: usize) -> Option<&T> {
106        if index >= self.len {
107            return None;
108        }
109        let (chunk, offset) = split_index(index);
110        self.chunks.get(&chunk)?.get(offset)?.as_ref()
111    }
112
113    /// Return whether `index` is an occupied in-range position.
114    pub fn contains_index(&self, index: usize) -> bool {
115        self.get(index).is_some()
116    }
117
118    /// Set `index`, growing the logical length with holes when necessary.
119    ///
120    /// Returns the previously stored value, if the position was occupied.
121    pub fn set(&mut self, index: usize, value: T) -> Result<Option<T>, SparseSequenceError> {
122        let required_len = index
123            .checked_add(1)
124            .ok_or(SparseSequenceError::IndexOverflow { index })?;
125        self.check_len(required_len)?;
126
127        let (chunk_index, offset) = split_index(index);
128        let chunk = self
129            .chunks
130            .entry(chunk_index)
131            .or_insert_with(|| Box::new(std::array::from_fn(|_| None)));
132        let previous = chunk[offset].replace(value);
133        if previous.is_none() {
134            self.occupied += 1;
135        }
136        self.len = self.len.max(required_len);
137        self.bump_revision();
138        Ok(previous)
139    }
140
141    /// Remove and return the value at `index`, leaving a stable hole.
142    pub fn remove(&mut self, index: usize) -> Option<T> {
143        if index >= self.len {
144            return None;
145        }
146        let (chunk_index, offset) = split_index(index);
147        let chunk = self.chunks.get_mut(&chunk_index)?;
148        let removed = chunk[offset].take()?;
149        self.occupied -= 1;
150        if chunk.iter().all(Option::is_none) {
151            self.chunks.remove(&chunk_index);
152        }
153        self.bump_revision();
154        Some(removed)
155    }
156
157    /// Set the logical length, creating holes on growth and dropping values on
158    /// truncation.
159    pub fn set_len(&mut self, new_len: usize) -> Result<(), SparseSequenceError> {
160        self.check_len(new_len)?;
161        if new_len == self.len {
162            return Ok(());
163        }
164        if new_len < self.len {
165            self.truncate_values(new_len);
166        }
167        self.len = new_len;
168        self.bump_revision();
169        Ok(())
170    }
171
172    /// Traverse occupied `(index, value)` pairs within a bounded index range.
173    ///
174    /// The range is intersected with the logical sequence, and holes do not
175    /// produce iterator items or work proportional to the logical length.
176    pub fn occupied_in<R>(&self, range: R) -> impl Iterator<Item = (usize, &T)>
177    where
178        R: RangeBounds<usize>,
179    {
180        let start = match range.start_bound() {
181            Bound::Included(index) => *index,
182            Bound::Excluded(index) => index.saturating_add(1),
183            Bound::Unbounded => 0,
184        }
185        .min(self.len);
186        let end = match range.end_bound() {
187            Bound::Included(index) => index.saturating_add(1),
188            Bound::Excluded(index) => *index,
189            Bound::Unbounded => self.len,
190        }
191        .min(self.len)
192        .max(start);
193        let first_chunk = start / CHUNK_LEN;
194        let end_chunk = end / CHUNK_LEN + usize::from(end % CHUNK_LEN != 0);
195
196        self.chunks
197            .range(first_chunk..end_chunk)
198            .flat_map(move |(chunk_index, chunk)| {
199                chunk.iter().enumerate().filter_map(move |(offset, value)| {
200                    let index = chunk_index * CHUNK_LEN + offset;
201                    (start..end)
202                        .contains(&index)
203                        .then(|| value.as_ref().map(|value| (index, value)))
204                        .flatten()
205                })
206            })
207    }
208
209    fn check_len(&self, requested: usize) -> Result<(), SparseSequenceError> {
210        if requested > self.max_len {
211            return Err(SparseSequenceError::LengthLimit {
212                requested,
213                limit: self.max_len,
214            });
215        }
216        Ok(())
217    }
218
219    fn truncate_values(&mut self, new_len: usize) {
220        let first_removed_chunk = new_len / CHUNK_LEN;
221        let first_removed_offset = new_len % CHUNK_LEN;
222
223        if first_removed_offset != 0
224            && let Some(chunk) = self.chunks.get_mut(&first_removed_chunk)
225        {
226            for slot in &mut chunk[first_removed_offset..] {
227                if slot.take().is_some() {
228                    self.occupied -= 1;
229                }
230            }
231            if chunk.iter().all(Option::is_none) {
232                self.chunks.remove(&first_removed_chunk);
233            }
234        }
235
236        let remove_from = first_removed_chunk + usize::from(first_removed_offset != 0);
237        let removed = self.chunks.split_off(&remove_from);
238        self.occupied -= removed
239            .values()
240            .map(|chunk| chunk.iter().filter(|slot| slot.is_some()).count())
241            .sum::<usize>();
242    }
243
244    fn bump_revision(&mut self) {
245        self.revision = self.revision.wrapping_add(1);
246    }
247}
248
249fn split_index(index: usize) -> (usize, usize) {
250    (index / CHUNK_LEN, index % CHUNK_LEN)
251}