hyperactor_mesh/v1/value_mesh/
value_overlay.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9use std::fmt;
10use std::ops::Range;
11
12use hyperactor::Named;
13use serde::Deserialize;
14use serde::Serialize;
15
16/// Builder error for overlays (structure only; region bounds are
17/// checked at merge time).
18///
19/// Note: serialization assumes identical pointer width between sender
20/// and receiver, as `Range<usize>` is not portable across
21/// architectures. TODO: introduce a wire‐stable run type.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub enum BuildError {
24    /// A run with an empty range (`start == end`) was provided.
25    EmptyRange,
26
27    /// Two runs overlap or are unsorted: `prev.end > next.start`. The
28    /// offending ranges are returned for debugging.
29    OverlappingRanges {
30        prev: Range<usize>,
31        next: Range<usize>,
32    },
33
34    /// A run exceeds the region bounds when applying an overlay
35    /// merge.
36    OutOfBounds {
37        range: Range<usize>,
38        region_len: usize,
39    },
40}
41
42impl fmt::Display for BuildError {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self {
45            BuildError::EmptyRange => {
46                write!(f, "a run with an empty range (start == end) was provided")
47            }
48            BuildError::OverlappingRanges { prev, next } => write!(
49                f,
50                "overlapping or unsorted runs: prev={:?}, next={:?}",
51                prev, next
52            ),
53            BuildError::OutOfBounds { range, region_len } => write!(
54                f,
55                "range {:?} exceeds region bounds (len={})",
56                range, region_len
57            ),
58        }
59    }
60}
61
62impl std::error::Error for BuildError {}
63
64/// A sparse overlay of rank ranges and values, used to assemble or
65/// patch a [`ValueMesh`] without materializing per-rank data.
66///
67/// Unlike `ValueMesh`, which always represents a complete, gap-free
68/// mapping over a [`Region`], a `ValueOverlay` is intentionally
69/// partial: it may describe only the ranks that have changed. This
70/// allows callers to build and merge small, incremental updates
71/// efficiently, while preserving the `ValueMesh` invariants after
72/// merge.
73///
74/// Invariants:
75/// - Runs are sorted by `(start, end)`.
76/// - Runs are non-empty and non-overlapping.
77/// - Adjacent runs with equal values are coalesced.
78/// - Region bounds are validated when the overlay is merged, not on
79///   insert.
80///
81/// Note: serialization assumes identical pointer width between sender
82/// and receiver, as `Range<usize>` is not portable across
83/// architectures. TODO: introduce a wire‐stable run type.
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Named, Default)]
85pub struct ValueOverlay<T> {
86    runs: Vec<(Range<usize>, T)>,
87}
88
89impl<T> ValueOverlay<T> {
90    /// Creates an empty overlay.
91    pub fn new() -> Self {
92        Self { runs: Vec::new() }
93    }
94
95    /// Returns an iterator over the internal runs.
96    pub fn runs(&self) -> impl Iterator<Item = &(Range<usize>, T)> {
97        self.runs.iter()
98    }
99
100    /// Current number of runs.
101    pub fn len(&self) -> usize {
102        self.runs.len()
103    }
104
105    /// Returns `true` if the overlay contains no runs. This indicates
106    /// that no ranges have been added — i.e., the overlay represents
107    /// an empty or no-op patch.
108    pub fn is_empty(&self) -> bool {
109        self.runs.is_empty()
110    }
111}
112
113impl<T: PartialEq> ValueOverlay<T> {
114    /// Adds a `(range, value)` run while maintaining the overlay
115    /// invariants.
116    ///
117    /// Fast path:
118    /// - If `range` is **after** the last run (`last.end <=
119    ///   range.start`), this is an O(1) append. If it **touches** the
120    ///   last run and `value` is equal, the two runs are **coalesced**
121    ///   by extending `end`.
122    ///
123    /// Slow path:
124    /// - If the input is **unsorted** or **overlaps** the last run,
125    ///   the method falls back to a full **normalize** (sort +
126    ///   coalesce + overlap check), making this call **O(n log n)**
127    ///   in the number of runs.
128    ///
129    /// Errors:
130    /// - Returns `BuildError::EmptyRange` if `range.is_empty()`.
131    ///
132    /// Notes & guidance:
133    /// - Use this for **already-sorted, non-overlapping** appends to
134    ///   get the cheap fast path.
135    /// - For **bulk or unsorted** inserts, prefer
136    ///   `ValueOverlay::try_from_runs` (collect → sort → coalesce) to
137    ///   avoid repeated re-normalization.
138    /// - Adjacent equal-value runs are coalesced automatically.
139    ///
140    /// # Examples
141    /// ```ignore
142    /// let mut ov = ValueOverlay::new();
143    /// ov.push_run(0..3, 1).unwrap();      // append
144    /// ov.push_run(3..5, 1).unwrap();      // coalesces to (0..5, 1)
145    ///
146    /// // Unsorted input triggers normalize (O(n log n)):
147    /// ov.push_run(1..2, 2).unwrap();      // re-sorts, checks overlaps, coalesces
148    /// ```
149    pub fn push_run(&mut self, range: Range<usize>, value: T) -> Result<(), BuildError> {
150        // Reject empty ranges.
151        if range.is_empty() {
152            return Err(BuildError::EmptyRange);
153        }
154
155        // Look at the last run.
156        match self.runs.last_mut() {
157            // The common case is appending in sorted order. Fast-path
158            // append if new run is after the last and
159            // non-overlapping.
160            Some((last_r, last_v)) if last_r.end <= range.start => {
161                if last_r.end == range.start && *last_v == value {
162                    // Coalesce equal-adjacent.
163                    last_r.end = range.end;
164                    return Ok(());
165                }
166                self.runs.push((range, value));
167                Ok(())
168            }
169            // The overlay was previously empty or, the caller
170            // inserted out of order (unsorted input).
171            _ => {
172                // Slow path. Re-sort, merge and validate the full
173                // runs vector.
174                self.runs.push((range, value));
175                Self::normalize(&mut self.runs)
176            }
177        }
178    }
179
180    /// Sorts, checks for overlaps, and coalesces equal-adjacent runs
181    /// in-place.
182    fn normalize(v: &mut Vec<(Range<usize>, T)>) -> Result<(), BuildError> {
183        // Early exit for empty overlays.
184        if v.is_empty() {
185            return Ok(());
186        }
187
188        // After this, ever later range has start >= prev.start. If
189        // any later start < prev.end it's an overlap.
190        v.sort_by_key(|(r, _)| (r.start, r.end));
191
192        // Build a fresh vector to collect cleaned using drain(..) on
193        // the input avoiding clone().
194        let mut out: Vec<(Range<usize>, T)> = Vec::with_capacity(v.len());
195        for (r, val) in v.drain(..) {
196            if let Some((prev_r, prev_v)) = out.last_mut() {
197                // If the next run's start is before the previous run's
198                // end we have an overlapping interval.
199                if r.start < prev_r.end {
200                    return Err(BuildError::OverlappingRanges {
201                        prev: prev_r.clone(),
202                        next: r,
203                    });
204                }
205                // If the previous run touches the new run and has the
206                // same value, merge them by extending the end
207                // boundary.
208                if prev_r.end == r.start && *prev_v == val {
209                    // Coalesce equal-adjacent.
210                    prev_r.end = r.end;
211                    continue;
212                }
213            }
214            // Otherwise, push as a new independent run.
215            out.push((r, val));
216        }
217
218        // Replace the old vector.
219        *v = out;
220
221        // Invariant: Runs is sorted, non-overlapping and coalesced.
222        Ok(())
223    }
224
225    /// Builds an overlay from arbitrary runs, validating structure
226    /// and coalescing equal-adjacent.
227    pub fn try_from_runs<I>(runs: I) -> Result<Self, BuildError>
228    where
229        I: IntoIterator<Item = (Range<usize>, T)>,
230    {
231        // We need a modifiable buffer to sort and normalize so we
232        // eagerly collect the iterator.
233        let mut v: Vec<(Range<usize>, T)> = runs.into_iter().collect();
234
235        // Reject empties up-front. Empty intervals are structurally
236        // invalid for an overlay. Fail fast.
237        for (r, _) in &v {
238            if r.is_empty() {
239                return Err(BuildError::EmptyRange);
240            }
241        }
242
243        // Sort by (start, end).
244        v.sort_by_key(|(r, _)| (r.start, r.end));
245
246        // Normalize (validate + coalesce).
247        Self::normalize(&mut v)?;
248
249        // Invariant: Runs is sorted, non-overlapping and coalesced.
250        Ok(Self { runs: v })
251    }
252}
253
254#[cfg(test)]
255mod tests {
256
257    use super::*;
258
259    #[test]
260    fn push_run_appends_and_coalesces() {
261        let mut ov = ValueOverlay::new();
262
263        // First insert.
264        ov.push_run(0..3, 1).unwrap();
265        assert_eq!(ov.runs, vec![(0..3, 1)]);
266
267        // Non-overlapping append.
268        ov.push_run(5..7, 2).unwrap();
269        assert_eq!(ov.runs, vec![(0..3, 1), (5..7, 2)]);
270
271        // Coalesce equal-adjacent (touching with same value).
272        ov.push_run(7..10, 2).unwrap();
273        assert_eq!(ov.runs, vec![(0..3, 1), (5..10, 2)]);
274    }
275
276    #[test]
277    fn push_run_detects_overlap() {
278        let mut ov = ValueOverlay::new();
279        ov.push_run(0..3, 1).unwrap();
280
281        // Overlaps 2..4 with existing 0..3.
282        let err = ov.push_run(2..4, 9).unwrap_err();
283        assert!(matches!(err, BuildError::OverlappingRanges { .. }));
284    }
285
286    #[test]
287    fn push_run_handles_unsorted_inserts() {
288        let mut ov = ValueOverlay::new();
289        // Insert out of order; normalize should sort and coalesce.
290        ov.push_run(10..12, 3).unwrap();
291        ov.push_run(5..8, 2).unwrap(); // Unsorted relative to last.
292        ov.push_run(8..10, 2).unwrap(); // Coalesce with previous.
293
294        assert_eq!(ov.runs, vec![(5..10, 2), (10..12, 3)]);
295    }
296
297    #[test]
298    fn try_from_runs_builds_and_coalesces() {
299        use super::ValueOverlay;
300
301        // Unsorted, with adjacent equal-value ranges that should
302        // coalesce.
303        let ov = ValueOverlay::try_from_runs(vec![(8..10, 2), (5..8, 2), (12..14, 3)]).unwrap();
304
305        assert_eq!(ov.runs, vec![(5..10, 2), (12..14, 3)]);
306    }
307
308    #[test]
309    fn try_from_runs_rejects_overlap_and_empty() {
310        // Overlap should error.
311        let err = ValueOverlay::try_from_runs(vec![(0..3, 1), (2..5, 2)]).unwrap_err();
312        assert!(matches!(err, BuildError::OverlappingRanges { .. }));
313
314        // Empty range should error.
315        let err = ValueOverlay::try_from_runs(vec![(0..0, 1)]).unwrap_err();
316        assert!(matches!(err, BuildError::EmptyRange));
317    }
318
319    #[test]
320    fn is_empty_reflects_state() {
321        let mut ov = ValueOverlay::<i32>::new();
322        assert!(ov.is_empty());
323
324        ov.push_run(0..1, 7).unwrap();
325        assert!(!ov.is_empty());
326    }
327
328    #[test]
329    fn normalize_sorts_coalesces_and_detects_overlap() {
330        // 1) Sort + coalesce equal-adjacent.
331        let mut v = vec![(5..7, 2), (3..5, 2), (7..9, 2)]; // unsorted, all value=2
332        ValueOverlay::<i32>::normalize(&mut v).unwrap();
333        assert_eq!(v, vec![(3..9, 2)]);
334
335        // 2) Overlap triggers error.
336        let mut v = vec![(3..6, 1), (5..8, 2)];
337        let err = ValueOverlay::<i32>::normalize(&mut v).unwrap_err();
338        assert!(matches!(err, BuildError::OverlappingRanges { .. }));
339    }
340}