hyperactor_mesh/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 serde::Deserialize;
13use serde::Serialize;
14use typeuri::Named;
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 (VO-1):
75/// - **VO-1 (sorted-runs):** 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    /// Consumes the overlay and returns the internal runs.
101    pub fn into_runs(self) -> Vec<(Range<usize>, T)> {
102        self.runs
103    }
104
105    /// Current number of runs.
106    pub fn len(&self) -> usize {
107        self.runs.len()
108    }
109
110    /// Returns `true` if the overlay contains no runs. This indicates
111    /// that no ranges have been added — i.e., the overlay represents
112    /// an empty or no-op patch.
113    pub fn is_empty(&self) -> bool {
114        self.runs.is_empty()
115    }
116}
117
118impl<T: PartialEq> ValueOverlay<T> {
119    /// Adds a `(range, value)` run while maintaining the overlay
120    /// invariants.
121    ///
122    /// Fast path:
123    /// - If `range` is **after** the last run (`last.end <=
124    ///   range.start`), this is an O(1) append. If it **touches** the
125    ///   last run and `value` is equal, the two runs are **coalesced**
126    ///   by extending `end`.
127    ///
128    /// Slow path:
129    /// - If the input is **unsorted** or **overlaps** the last run,
130    ///   the method falls back to a full **normalize** (sort +
131    ///   coalesce + overlap check), making this call **O(n log n)**
132    ///   in the number of runs.
133    ///
134    /// Errors:
135    /// - Returns `BuildError::EmptyRange` if `range.is_empty()`.
136    ///
137    /// Notes & guidance:
138    /// - Use this for **already-sorted, non-overlapping** appends to
139    ///   get the cheap fast path.
140    /// - For **bulk or unsorted** inserts, prefer
141    ///   `ValueOverlay::try_from_runs` (collect → sort → coalesce) to
142    ///   avoid repeated re-normalization.
143    /// - Adjacent equal-value runs are coalesced automatically.
144    ///
145    /// # Examples
146    /// ```ignore
147    /// let mut ov = ValueOverlay::new();
148    /// ov.push_run(0..3, 1).unwrap();      // append
149    /// ov.push_run(3..5, 1).unwrap();      // coalesces to (0..5, 1)
150    ///
151    /// // Unsorted input triggers normalize (O(n log n)):
152    /// ov.push_run(1..2, 2).unwrap();      // re-sorts, checks overlaps, coalesces
153    /// ```
154    pub fn push_run(&mut self, range: Range<usize>, value: T) -> Result<(), BuildError> {
155        // Reject empty ranges.
156        if range.is_empty() {
157            return Err(BuildError::EmptyRange);
158        }
159
160        // Look at the last run.
161        match self.runs.last_mut() {
162            // The common case is appending in sorted order. Fast-path
163            // append if new run is after the last and
164            // non-overlapping.
165            Some((last_r, last_v)) if last_r.end <= range.start => {
166                if last_r.end == range.start && *last_v == value {
167                    // Coalesce equal-adjacent.
168                    last_r.end = range.end;
169                    return Ok(());
170                }
171                self.runs.push((range, value));
172                Ok(())
173            }
174            // The overlay was previously empty or, the caller
175            // inserted out of order (unsorted input).
176            _ => {
177                // Slow path. Re-sort, merge and validate the full
178                // runs vector.
179                self.runs.push((range, value));
180                Self::normalize(&mut self.runs)
181            }
182        }
183    }
184
185    /// Sorts, checks for overlaps, and coalesces equal-adjacent runs
186    /// in-place.
187    fn normalize(v: &mut Vec<(Range<usize>, T)>) -> Result<(), BuildError> {
188        // Early exit for empty overlays.
189        if v.is_empty() {
190            return Ok(());
191        }
192
193        // After this, ever later range has start >= prev.start. If
194        // any later start < prev.end it's an overlap.
195        v.sort_by_key(|(r, _)| (r.start, r.end));
196
197        // Build a fresh vector to collect cleaned using drain(..) on
198        // the input avoiding clone().
199        let mut out: Vec<(Range<usize>, T)> = Vec::with_capacity(v.len());
200        for (r, val) in v.drain(..) {
201            if let Some((prev_r, prev_v)) = out.last_mut() {
202                // If the next run's start is before the previous run's
203                // end we have an overlapping interval.
204                if r.start < prev_r.end {
205                    return Err(BuildError::OverlappingRanges {
206                        prev: prev_r.clone(),
207                        next: r,
208                    });
209                }
210                // If the previous run touches the new run and has the
211                // same value, merge them by extending the end
212                // boundary.
213                if prev_r.end == r.start && *prev_v == val {
214                    // Coalesce equal-adjacent.
215                    prev_r.end = r.end;
216                    continue;
217                }
218            }
219            // Otherwise, push as a new independent run.
220            out.push((r, val));
221        }
222
223        // Replace the old vector.
224        *v = out;
225
226        // VO-1: runs are sorted, non-overlapping and coalesced.
227        Ok(())
228    }
229
230    /// Builds an overlay from arbitrary runs, validating structure
231    /// and coalescing equal-adjacent.
232    pub fn try_from_runs<I>(runs: I) -> Result<Self, BuildError>
233    where
234        I: IntoIterator<Item = (Range<usize>, T)>,
235    {
236        // We need a modifiable buffer to sort and normalize so we
237        // eagerly collect the iterator.
238        let mut v: Vec<(Range<usize>, T)> = runs.into_iter().collect();
239
240        // Reject empties up-front. Empty intervals are structurally
241        // invalid for an overlay. Fail fast.
242        for (r, _) in &v {
243            if r.is_empty() {
244                return Err(BuildError::EmptyRange);
245            }
246        }
247
248        // Sort by (start, end).
249        v.sort_by_key(|(r, _)| (r.start, r.end));
250
251        // Normalize (validate + coalesce).
252        Self::normalize(&mut v)?;
253
254        // VO-1: runs are sorted, non-overlapping and coalesced.
255        Ok(Self { runs: v })
256    }
257}
258
259#[cfg(test)]
260mod tests {
261
262    use super::*;
263
264    #[test]
265    fn push_run_appends_and_coalesces() {
266        let mut ov = ValueOverlay::new();
267
268        // First insert.
269        ov.push_run(0..3, 1).unwrap();
270        assert_eq!(ov.runs, vec![(0..3, 1)]);
271
272        // Non-overlapping append.
273        ov.push_run(5..7, 2).unwrap();
274        assert_eq!(ov.runs, vec![(0..3, 1), (5..7, 2)]);
275
276        // Coalesce equal-adjacent (touching with same value).
277        ov.push_run(7..10, 2).unwrap();
278        assert_eq!(ov.runs, vec![(0..3, 1), (5..10, 2)]);
279    }
280
281    #[test]
282    fn push_run_detects_overlap() {
283        let mut ov = ValueOverlay::new();
284        ov.push_run(0..3, 1).unwrap();
285
286        // Overlaps 2..4 with existing 0..3.
287        let err = ov.push_run(2..4, 9).unwrap_err();
288        assert!(matches!(err, BuildError::OverlappingRanges { .. }));
289    }
290
291    #[test]
292    fn push_run_handles_unsorted_inserts() {
293        let mut ov = ValueOverlay::new();
294        // Insert out of order; normalize should sort and coalesce.
295        ov.push_run(10..12, 3).unwrap();
296        ov.push_run(5..8, 2).unwrap(); // Unsorted relative to last.
297        ov.push_run(8..10, 2).unwrap(); // Coalesce with previous.
298
299        assert_eq!(ov.runs, vec![(5..10, 2), (10..12, 3)]);
300    }
301
302    #[test]
303    fn try_from_runs_builds_and_coalesces() {
304        use super::ValueOverlay;
305
306        // Unsorted, with adjacent equal-value ranges that should
307        // coalesce.
308        let ov = ValueOverlay::try_from_runs(vec![(8..10, 2), (5..8, 2), (12..14, 3)]).unwrap();
309
310        assert_eq!(ov.runs, vec![(5..10, 2), (12..14, 3)]);
311    }
312
313    #[test]
314    fn try_from_runs_rejects_overlap_and_empty() {
315        // Overlap should error.
316        let err = ValueOverlay::try_from_runs(vec![(0..3, 1), (2..5, 2)]).unwrap_err();
317        assert!(matches!(err, BuildError::OverlappingRanges { .. }));
318
319        // Empty range should error.
320        let err = ValueOverlay::try_from_runs(vec![(0..0, 1)]).unwrap_err();
321        assert!(matches!(err, BuildError::EmptyRange));
322    }
323
324    #[test]
325    fn is_empty_reflects_state() {
326        let mut ov = ValueOverlay::<i32>::new();
327        assert!(ov.is_empty());
328
329        ov.push_run(0..1, 7).unwrap();
330        assert!(!ov.is_empty());
331    }
332
333    #[test]
334    fn normalize_sorts_coalesces_and_detects_overlap() {
335        // 1) Sort + coalesce equal-adjacent.
336        let mut v = vec![(5..7, 2), (3..5, 2), (7..9, 2)]; // unsorted, all value=2
337        ValueOverlay::<i32>::normalize(&mut v).unwrap();
338        assert_eq!(v, vec![(3..9, 2)]);
339
340        // 2) Overlap triggers error.
341        let mut v = vec![(3..6, 1), (5..8, 2)];
342        let err = ValueOverlay::<i32>::normalize(&mut v).unwrap_err();
343        assert!(matches!(err, BuildError::OverlappingRanges { .. }));
344    }
345}