tor_netdoc/util/rangemap_ext.rs
1//! Read-modify-write values for a contiguous range of keys, in a range map
2//!
3//! Separate module mostly so that the tests have somewhere convenient to live.
4//!
5//! Arguably this should be in another crate.
6//! But it brings in rangemap as a dependency which seems undesirable for tor-basic-utils.
7//! Right now it is here because this is where its (lowest in stack) call site will be.
8
9use std::ops::{Bound, RangeInclusive};
10
11use itertools::{Itertools, chain};
12use rangemap::{RangeInclusiveMap, StepFns, StepLite};
13
14use tor_basic_utils::rangebounds::RangeBoundsExt;
15
16/// Read-modify-write values for a contiguous range of keys, in a range map
17///
18/// Since the map might contain different values for various parts of the specified range,
19/// multiple possible old values might need to be handled.
20/// `rangemap_mutate_range` is suitable if different old values can be handled independently,
21/// or sequentially.
22///
23/// Calls `update` for every range currently in the map overlapping with `range0`,
24/// and for every gap overlapping with `range`.
25///
26/// If the mutated value is equal (`PartialEq`), no actual update is made.
27///
28/// If `update` throws `Err`, any mutations to its first argument *will* be stored in the map,
29/// but no further calls to `update` will be made (so only part of the range
30/// might be updated).
31///
32/// `update` should probably not use the provided `&RangeInclusive` argument as an input
33/// to calculating how to update the `&mut V`. Doing so would make the results depend
34/// on the details of range fragmentation in the rangemap,
35/// which would be inconsistent with the usual use of a rangemap as an optimisation
36/// of an abstract data structure which stores a separate value for each key.
37///
38/// Note that `update` doesn't get a mutable reference *into the map*.
39/// so if it mutates its argument and then panics, the update might not be applied.
40// RangeInclusiveMap doesn't provide any in-place update API, and an in-place update is also
41// incompatible with passing `&mut Option<V>`.
42//
43// Other APIs that were considered, include:
44//
45// * FnMut(Option<V>) -> Option<V>
46//
47// This seems less idiomatic.
48//
49// * Don't coalesce equal values, skipping a comparison.
50//
51// `RangeInclusiveMap` already coalesces adjacent identical ranges.
52// Comparing the old and new value is O(sizeof(V)), whereas RangeInclusiveMap::insert
53// is at least O(log N) and will often involves that comparison against adjacent ranges.
54//
55// In theory this optimisation might be done by `RangeInclusiveMap` already, but it's
56// not mentioned. Probably, Eq is quite cheap.
57pub fn rangemap_mutate_range<K, V, StepFnsT, E>(
58 map: &mut RangeInclusiveMap<K, V, StepFnsT>,
59 range0: &RangeInclusive<K>,
60 mut update: impl FnMut(&mut Option<V>, &RangeInclusive<K>) -> Result<(), E>,
61) -> Result<(), E>
62where
63 K: Ord + Clone + StepLite,
64 V: PartialEq + Clone,
65 StepFnsT: StepFns<K>,
66{
67 let relevants = chain!(
68 map.overlapping(range0).map(|(k, _v)| k.clone()),
69 map.gaps(range0),
70 )
71 .collect_vec();
72
73 for relevant in relevants {
74 // `relevant` is a range stored in the rangemap, or a gap, which overlaps with `range0`,
75 // but which might be bigger or smaller (or both) than `range0`.
76 //
77 // Insofar as it's smaller, then if there are other ranges in the rangemap with overlap,
78 // they'll be handled separately, in other loop iterations.
79 //
80 // But insofar as it's bigger, we need to trim it down, because we're only supposed
81 // to modify `range0`.
82 //
83 // We distinguish `k`, the range we are updating in this iteration,
84 // from `range0`, the overall range from our caller.
85 let k = {
86 let k = relevant
87 .intersect(range0)
88 .expect("intersection of overlapping ranges was empty");
89
90 // RangeBoundsExt::intersect works at the level of the RangeBounds trait,
91 // returning `Option<(Bound, Bound)>`, This helper closure converts one of the
92 // returned `Bound`s back to a plain value. The returned intersection will
93 // always be an inclusive range because the input ranges were inclusive
94 // (ie, closed, in set theory terms) and intersection preserves set closedness.
95 let fix = |b: Bound<&K>| match b {
96 Bound::Included(y) => y.clone(),
97 _other => unreachable!("intersection of closed ranges wasn't closed"),
98 };
99
100 fix(k.0)..=fix(k.1)
101 };
102
103 let v0 = map.get(k.start());
104 let mut v = v0.cloned();
105 // avoid losing an update if Err is returned, so park any `Err` in `r`
106 let r = update(&mut v, &k);
107 if v.as_ref() != v0 {
108 if let Some(v) = v {
109 map.insert(k, v);
110 } else {
111 map.remove(k);
112 }
113 }
114 r?;
115 }
116 Ok(())
117}
118
119#[cfg(test)]
120mod test {
121 // @@ begin test lint list maintained by maint/add_warning @@
122 #![allow(clippy::bool_assert_comparison)]
123 #![allow(clippy::clone_on_copy)]
124 #![allow(clippy::dbg_macro)]
125 #![allow(clippy::mixed_attributes_style)]
126 #![allow(clippy::print_stderr)]
127 #![allow(clippy::print_stdout)]
128 #![allow(clippy::single_char_pattern)]
129 #![allow(clippy::unwrap_used)]
130 #![allow(clippy::unchecked_time_subtraction)]
131 #![allow(clippy::useless_vec)]
132 #![allow(clippy::needless_pass_by_value)]
133 #![allow(clippy::string_slice)] // See arti#2571
134 //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
135 use super::*;
136 use educe::Educe;
137 use std::fmt::Debug;
138 use void::{ResultVoidExt as _, Void};
139
140 type Range = RangeInclusive<u8>;
141 const ALL_K: Range = 0..=255;
142 type Id = u32;
143
144 /// Magic value
145 ///
146 /// Comparisons use only the value in `v`, but we track its value identity,
147 /// which lets us see (for example) whether an "equal" update did anything.
148 #[derive(Debug, Clone, Educe)]
149 #[educe(PartialEq)]
150 struct Value {
151 v: char,
152 #[educe(PartialEq(ignore))]
153 id: Id,
154 }
155
156 /// Test wrapper for `RangeInclusiveMap`
157 ///
158 /// Maintains a separate copy of the expected current V for each K, in `reference`.
159 /// Cross-checks it.
160 #[derive(Debug, Clone, Educe)]
161 #[educe(Default, PartialEq)]
162 struct TestState {
163 map: RangeInclusiveMap<u8, Value>,
164 #[educe(Default(expression = "[None; _]"))] // Only short arrays are Default :-/
165 reference: [Option<char>; 256],
166 #[educe(PartialEq(ignore))]
167 ids: IdGenerator,
168 }
169
170 /// avoids constant repetition of `let ; += 1;` pattern
171 #[derive(Debug, Clone, Default, PartialEq, Eq)]
172 struct IdGenerator(Id);
173
174 impl IdGenerator {
175 fn next(&mut self) -> Id {
176 let r = self.0;
177 self.0 += 1;
178 r
179 }
180 }
181
182 impl TestState {
183 fn from_iter(elems: impl IntoIterator<Item = (Range, char)>) -> Self {
184 let mut self_ = TestState::default();
185 let id = self_.ids.next();
186 for (range, v) in elems {
187 self_.map.insert(range.clone(), Value { v, id });
188 for k in range.clone() {
189 self_.reference[k as usize] = Some(v);
190 }
191 }
192 self_
193 }
194
195 /// Calls `rangemap_mutate_range`, but also updates `reference`, makes some checks, etc.
196 fn mutate_range<E: Debug>(
197 &mut self,
198 range0: Range,
199 mut real_update: impl FnMut(&Range, &mut Option<Value>) -> Result<(), E>,
200 ) -> Result<(), E> {
201 let mut updated = [false; 256];
202 println!("updating range0={range0:?}");
203
204 let r = rangemap_mutate_range(
205 &mut self.map,
206 &range0,
207 |value: &mut Option<Value>, range| {
208 println!("updating range0={range:?} range={range:?}");
209 assert!(range0.contains(range.start()), "uncontained start");
210 assert!(range0.contains(range.end()), "uncontained end");
211 let r = real_update(range, value);
212 println!("updating range0={range0:?} range={range:?}, to {value:?}, r={r:?}");
213 for k in range.clone() {
214 updated[k as usize] = true;
215 self.reference[k as usize] = value.as_ref().map(|value| value.v);
216 }
217 r
218 },
219 );
220 self.check();
221
222 if r.is_ok() {
223 for k in ALL_K {
224 assert_eq!(
225 updated[k as usize],
226 range0.contains(&k),
227 "updated inconsistency k={k:?}",
228 );
229 }
230 }
231
232 r
233 }
234
235 fn set_range(&mut self, range0: Range, val: char, expected_old_values: &str) {
236 let id = self.ids.next();
237 self.mutate_range(range0.clone(), |k, vmut| {
238 assert!(
239 expected_old_values.contains(vmut.as_ref().map(|v| v.v).unwrap_or('_')),
240 "{range0:?} {k:?} {val:?} {vmut:?} {expected_old_values:?}"
241 );
242 *vmut = Some(Value { v: val, id });
243 Ok::<_, Void>(())
244 })
245 .void_unwrap();
246 }
247
248 fn check(&self) {
249 for k in ALL_K {
250 assert_eq!(
251 self.map.get(&k).map(|v| v.v),
252 self.reference[k as usize],
253 "map now implies wrong v at k={k:?}",
254 );
255 }
256 }
257 }
258
259 #[test]
260 fn mutations() {
261 let s0 = TestState::from_iter([
262 //
263 (0..=9, 'a'),
264 (20..=29, 'x'),
265 ]);
266
267 {
268 // mutate precisely an existing range
269 let mut s = s0.clone();
270 s.set_range(0..=9, 'b', "a");
271 }
272 {
273 // mutate precisely a gap
274 let mut s = s0.clone();
275 s.mutate_range(10..=19, |_k, v| {
276 assert_eq!(*v, None);
277 Ok::<_, Void>(())
278 })
279 .void_unwrap();
280 assert_eq!(s, s0);
281 s.set_range(10..=19, 'n', "_");
282 }
283 {
284 // mutate strictly a subset of an existing range
285 let mut s = s0.clone();
286 s.set_range(1..=8, 'b', "a");
287 // now mutate parts of several ranges, with no gap in between, including a singleton
288 s.set_range(7..=9, 'c', "ab");
289 }
290 {
291 // mutate parts of several ranges, with a gap in between
292 let mut s = s0.clone();
293 s.set_range(5..=25, 'm', "_ax");
294 }
295 {
296 // mutate parts of several ranges, throwing and error halfway through
297 let mut s = s0.clone();
298 s.mutate_range(5..=25, |k, v| {
299 *v = Some(Value { v: 'm', id: 1000 });
300 (*k.start() < 15).then_some(()).ok_or(())
301 })
302 .expect_err("terminated early");
303 }
304 {
305 let mut s = s0.clone();
306 let range = 0..=9;
307 // overwrite the whole range with the same value
308 // if the underlying insert call were made,
309 // the value in the range would be replaced
310 s.set_range(range.clone(), 'a', "a");
311 // check that the thing at 1, the start of the "mutated" range,
312 // is in fact *not* the value we inserted but the existing one.
313 // this checks that our own PartialEq skip is workign
314 let ent = s.map.get(&1).unwrap();
315 assert_eq!(ent.id, 0);
316 // check that our assumption about rangemap is true
317 let id = s.ids.next();
318 s.map.insert(range.clone(), Value { v: 'a', id });
319 let ent = s.map.get(&1).unwrap();
320 assert_eq!(ent.id, id);
321 }
322 }
323}