Skip to main content

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