Skip to main content

ndslice/
selection.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
9//! This module defines a recursive algebra for selecting coordinates
10//! in a multidimensional space.
11//!
12//! A `Selection` describes constraints across dimensions of an
13//! `ndslice::Slice`. Variants like [`All`], [`First`], and [`Range`]
14//! operate dimensionally, while [`Intersection`] and [`Union`] allow
15//! for logical composition of selections.
16//!
17//! ## Example
18//!
19//! Suppose a 3-dimensional mesh system of:
20//! - 2 zones
21//! - 4 hosts per zone
22//! - 8 GPUs per host
23//!
24//! The corresponding `Slice` will have shape `[2, 4, 8]`. An
25//! expression to denote the first 4 GPUs of host 0 together with the
26//! last 4 GPUs on host 1 across all regions can be written as:
27//! ```rust
28//! use ndslice::selection::dsl::all;
29//! use ndslice::selection::dsl::range;
30//! use ndslice::selection::dsl::true_;
31//! use ndslice::selection::dsl::union;
32//!
33//! let s = all(range(0, range(0..4, true_())));
34//! let t = all(range(1, range(4.., true_())));
35//! let selection = union(s, t);
36//! ```
37//! Assuming a row-major layout, that is the set of 4 x 2 x 2 = 16
38//! coordinates *{(0, 0, 0), ... (0, 0, 3), (0, 1, 5), ..., (0, 1, 7),
39//! (1, 0, 0), ..., (1, 0, 3), (1, 1, 4), ..., (1, 1, 7)}* where code
40//! to print that set might read as follows.
41//! ```rust
42//! use ndslice::Slice;
43//! use ndslice::selection::EvalOpts;
44//! use ndslice::selection::dsl::all;
45//! use ndslice::selection::dsl::range;
46//! use ndslice::selection::dsl::true_;
47//! use ndslice::selection::dsl::union;
48//!
49//! let slice = Slice::new(0usize, vec![2, 4, 8], vec![32, 8, 1]).unwrap();
50//! let s = all(range(0, range(0..4, true_())));
51//! let t = all(range(1, range(4.., true_())));
52//!
53//! for r in union(s, t).eval(&EvalOpts::lenient(), &slice).unwrap() {
54//!     println!("{:?}", slice.coordinates(r).unwrap());
55//! }
56//! ```
57//! which is using the `eval` function described next.
58//!
59//! ## Evaluation
60//!
61//! Selections are evaluated against an `ndslice::Slice` using the
62//! [`Selection::eval`] method, which returns a lazy iterator over the
63//! flat (linearized) indices of elements that match.
64//!
65//! Evaluation proceeds recursively, dimension by dimension, with each
66//! variant of `Selection` contributing logic at a particular level of
67//! the slice.
68//!
69//! If a `Selection` is shorter than the number of dimensions, it is
70//! implicitly extended with `true_()` at the remaining levels. This
71//! means `Selection::True` acts as the identity element, matching all
72//! remaining indices by default.
73
74/// A parser for selection expressions in a compact textual syntax.
75///
76/// See [`selection::parse`] for syntax details and examples.
77pub mod parse;
78
79/// Formatting utilities for `Selection` expressions.
80///
81/// This module defines pretty-printers and compact syntax renderers
82/// for selections, based on implementations of the `SelectionSYM`
83/// trait.
84///
85/// The `Display` implementation for [`Selection`] uses this module.
86pub mod pretty;
87
88/// A `TokenStream` to [`Selection`] parser. Used at compile time in
89/// [`sel!]`. See [`selection::parse`] for syntax details and
90/// examples.
91pub mod token_parser;
92
93/// Normalization logic for `Selection`.
94pub mod normal;
95
96pub mod test_utils;
97
98use std::collections::BTreeSet;
99use std::collections::HashMap;
100use std::collections::HashSet;
101use std::fmt;
102
103use rand::RngExt as _;
104use serde::Deserialize;
105use serde::Serialize;
106
107use crate::Slice;
108use crate::selection::normal::NormalizedSelection;
109use crate::selection::normal::RewriteRuleExt;
110use crate::shape;
111use crate::shape::ShapeError;
112use crate::slice::SliceError;
113
114/// This trait defines an abstract syntax without committing to a
115/// specific representation. It follow the
116/// [tagless-final](https://okmij.org/ftp/tagless-final/index.html)
117/// style where [`Selection`] is a default AST representation.
118pub trait SelectionSYM {
119    /// The identity selection (matches no nodes).
120    fn false_() -> Self;
121
122    /// The universal selection (matches all nodes).
123    fn true_() -> Self;
124
125    /// Selects all values along the current dimension, then applies
126    /// the inner selection.
127    fn all(selection: Self) -> Self;
128
129    /// Selects the first index along the current dimension for which
130    /// the inner selection is non-empty.
131    fn first(selection: Self) -> Self;
132
133    /// Selects values within the given range along the current
134    /// dimension, then applies the inner selection.
135    fn range<R: Into<shape::Range>>(range: R, selection: Self) -> Self;
136
137    /// Selects values along the current dimension that match the
138    /// given labels, then applies the inner selection.
139    fn label<L: Into<LabelKey>>(labels: Vec<L>, selection: Self) -> Self;
140
141    /// Selects a random index along the current dimension, then applies
142    /// the inner selection.
143    fn any(selection: Self) -> Self;
144
145    /// The intersection (logical AND) of two selection expressions.
146    fn intersection(lhs: Self, selection: Self) -> Self;
147
148    /// The union (logical OR) of two selection expressions.
149    fn union(lhs: Self, selection: Self) -> Self;
150}
151
152/// `SelectionSYM`-based constructors specialized to the [`Selection`]
153/// AST.
154pub mod dsl {
155
156    use super::LabelKey;
157    use super::Selection;
158    use super::SelectionSYM;
159    use crate::shape;
160
161    pub fn false_() -> Selection {
162        SelectionSYM::false_()
163    }
164    pub fn true_() -> Selection {
165        SelectionSYM::true_()
166    }
167    pub fn all(inner: Selection) -> Selection {
168        SelectionSYM::all(inner)
169    }
170    pub fn first(inner: Selection) -> Selection {
171        SelectionSYM::first(inner)
172    }
173    pub fn range<R: Into<shape::Range>>(r: R, inner: Selection) -> Selection {
174        SelectionSYM::range(r, inner)
175    }
176    pub fn label<L: Into<LabelKey>>(labels: Vec<L>, inner: Selection) -> Selection {
177        SelectionSYM::label(labels, inner)
178    }
179    pub fn any(inner: Selection) -> Selection {
180        SelectionSYM::any(inner)
181    }
182    pub fn intersection(lhs: Selection, rhs: Selection) -> Selection {
183        SelectionSYM::intersection(lhs, rhs)
184    }
185    pub fn union(lhs: Selection, rhs: Selection) -> Selection {
186        SelectionSYM::union(lhs, rhs)
187    }
188}
189
190impl SelectionSYM for Selection {
191    fn false_() -> Self {
192        ast::false_()
193    }
194    fn true_() -> Self {
195        ast::true_()
196    }
197    fn all(selection: Self) -> Self {
198        ast::all(selection)
199    }
200    fn first(selection: Self) -> Self {
201        ast::first(selection)
202    }
203    fn range<R: Into<shape::Range>>(range: R, selection: Self) -> Self {
204        ast::range(range, selection)
205    }
206    fn label<L: Into<LabelKey>>(labels: Vec<L>, selection: Selection) -> Selection {
207        let labels = labels.into_iter().map(|l| l.into()).collect();
208        Selection::Label(labels, Box::new(selection))
209    }
210    fn any(selection: Self) -> Self {
211        ast::any(selection)
212    }
213    fn intersection(lhs: Self, rhs: Self) -> Self {
214        ast::intersection(lhs, rhs)
215    }
216    fn union(lhs: Self, rhs: Self) -> Self {
217        ast::union(lhs, rhs)
218    }
219}
220
221impl fmt::Display for Selection {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        write!(f, "{}", pretty::pretty(self))
224    }
225}
226
227/// A metadata label used to constrain values at a given coordinate
228/// dimension.
229///
230/// `LabelKey` represents attribute values associated with indices —
231/// for example, GPU model names like `"A100"` or capabilities like
232/// "AVX-512".
233///
234/// Labels are not dimension names (like `"zone"` or `"rack"`); they
235/// are **values** assigned to elements at a given dimension, and are
236/// used by `Selection::Label` to restrict which values are eligible
237/// during selection or routing.
238/// For example, a selection like `sel!(["A100"]*)` matches only
239/// indices at the current dimension whose associated label value is
240/// `"A100"`.
241///
242/// `Ord` is derived to allow deterministic sorting and set membership,
243/// based on lexicographic ordering of label strings.
244#[derive(
245    Clone,
246    Debug,
247    PartialEq,
248    Eq,
249    Hash,
250    Serialize,
251    Deserialize,
252    PartialOrd,
253    Ord
254)]
255pub enum LabelKey {
256    /// A plain string label value.
257    Value(String),
258}
259
260impl From<String> for LabelKey {
261    fn from(s: String) -> Self {
262        LabelKey::Value(s)
263    }
264}
265
266impl From<&str> for LabelKey {
267    fn from(s: &str) -> Self {
268        LabelKey::Value(s.to_string())
269    }
270}
271
272impl std::fmt::Display for LabelKey {
273    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274        match self {
275            LabelKey::Value(s) => write!(f, "\"{}\"", s),
276        }
277    }
278}
279
280/// An algebra for expressing node selection.
281#[derive(Debug, Clone, Serialize, Deserialize)]
282#[non_exhaustive]
283pub enum Selection {
284    /// A selection that never matches any node.
285    False,
286
287    /// A selection that always matches any node.
288    True,
289
290    /// Selects all values along the current dimension, continuing
291    /// with the given selection.
292    All(Box<Selection>),
293
294    /// Selects the first value along the current dimension for which
295    /// applying the inner selection yields any results.
296    First(Box<Selection>),
297
298    /// Selects values within a given range along the current
299    /// dimension, continuing with the given selection.
300    Range(shape::Range, Box<Selection>),
301
302    /// Selects values based on metadata (i.e., labels) along the
303    /// current dimension. This provides attribute-based selection.
304    Label(Vec<LabelKey>, Box<Selection>),
305
306    /// Selects a random index along the current dimension, continuing
307    /// with the given selection.
308    Any(Box<Selection>),
309
310    /// The intersection (logical AND) of two selections.
311    Intersection(Box<Selection>, Box<Selection>),
312
313    /// The union (logical OR) of two selections.
314    Union(Box<Selection>, Box<Selection>),
315}
316
317// Compile-time check: ensure Selection is thread-safe and fully
318// owned.
319fn _assert_selection_traits()
320where
321    Selection: Send + Sync + 'static,
322{
323}
324
325/// Compares two `Selection` values for structural equality.
326///
327/// Two selections are structurally equal if they have the same shape
328/// and recursively equivalent substructure, but not necessarily the
329/// same pointer identity or formatting.
330pub fn structurally_equal(a: &Selection, b: &Selection) -> bool {
331    use Selection::*;
332    match (a, b) {
333        (False, False) => true,
334        (True, True) => true,
335        (All(x), All(y)) => structurally_equal(x, y),
336        (Any(x), Any(y)) => structurally_equal(x, y),
337        (First(x), First(y)) => structurally_equal(x, y),
338        (Range(r1, x), Range(r2, y)) => r1 == r2 && structurally_equal(x, y),
339        (Intersection(x1, y1), Intersection(x2, y2)) => {
340            structurally_equal(x1, x2) && structurally_equal(y1, y2)
341        }
342        (Union(x1, y1), Union(x2, y2)) => structurally_equal(x1, x2) && structurally_equal(y1, y2),
343        _ => false,
344    }
345}
346
347/// Normalizes a [`Selection`] toward a canonical form for structural
348/// comparison.
349///
350/// This rewrites the selection to eliminate redundant subtrees and
351/// bring structurally similar selections into a common
352/// representation. The result is suitable for comparison, hashing,
353/// and deduplication (e.g., in [`NormalizedSelectionKey`]).
354///
355/// Normalization preserves semantics but may alter syntactic
356/// structure. It is designed to improve over time as additional
357/// rewrites (e.g., flattening, simplification) are introduced.
358pub fn normalize(sel: &Selection) -> NormalizedSelection {
359    let rule = normal::FlatteningRules
360        .then(normal::IdentityRules)
361        .then(normal::AbsorbtionRules);
362    sel.fold::<normal::NormalizedSelection>()
363        .rewrite_bottom_up(&rule)
364}
365
366/// Wraps a normalized selection and derives `Eq` and `Hash`, relying
367/// on the canonical structure of the normalized form.
368///
369/// This ensures that logically equivalent selections (e.g., unions
370/// with reordered elements) compare equal and hash identically.
371#[derive(Debug, Clone, PartialEq, Eq, Hash)]
372pub struct NormalizedSelectionKey(NormalizedSelection);
373
374impl NormalizedSelectionKey {
375    /// Constructs a `NormalizedSelectionKey`, normalizing the input
376    /// selection.
377    pub fn new(sel: &Selection) -> Self {
378        Self(crate::selection::normalize(sel))
379    }
380
381    /// Access the normalized selection.
382    pub fn inner(&self) -> &NormalizedSelection {
383        &self.0
384    }
385
386    /// Consumes the key and returns the owned normalized selection.
387    pub fn into_inner(self) -> NormalizedSelection {
388        self.0
389    }
390}
391
392mod ast {
393    use super::LabelKey;
394    use super::Selection;
395    use crate::shape;
396
397    pub(crate) fn false_() -> Selection {
398        Selection::False
399    }
400    pub(crate) fn true_() -> Selection {
401        Selection::True
402    }
403    pub(crate) fn all(selection: Selection) -> Selection {
404        Selection::All(Box::new(selection))
405    }
406    pub(crate) fn first(selection: Selection) -> Selection {
407        Selection::First(Box::new(selection))
408    }
409    pub(crate) fn range<R: Into<shape::Range>>(range: R, selection: Selection) -> Selection {
410        Selection::Range(range.into(), Box::new(selection))
411    }
412    #[allow(dead_code)] // Harmless.
413    pub(crate) fn label<L: Into<LabelKey>>(labels: Vec<L>, selection: Selection) -> Selection {
414        let labels = labels.into_iter().map(Into::into).collect();
415        Selection::Label(labels, Box::new(selection))
416    }
417    pub(crate) fn any(selection: Selection) -> Selection {
418        Selection::Any(Box::new(selection))
419    }
420    pub(crate) fn intersection(lhs: Selection, rhs: Selection) -> Selection {
421        Selection::Intersection(Box::new(lhs), Box::new(rhs))
422    }
423    pub(crate) fn union(lhs: Selection, rhs: Selection) -> Selection {
424        Selection::Union(Box::new(lhs), Box::new(rhs))
425    }
426}
427
428/// `EvalOpts` controls runtime behavior of [`Selection::eval`] by
429/// enforcing stricter validation rules.
430pub struct EvalOpts {
431    /// Fail `eval` on empty range expressions.
432    pub disallow_empty_ranges: bool,
433
434    /// Fail `eval` on a range beginning after the slice's extent in
435    /// the evaluation context's dimension.
436    pub disallow_out_of_range: bool,
437
438    /// Fail `eval` if a selection can be shown to be not "static".
439    pub disallow_dynamic_selections: bool,
440}
441
442impl EvalOpts {
443    // Produce empty iterators but don't panic.
444    pub fn lenient() -> Self {
445        Self {
446            disallow_empty_ranges: false,
447            disallow_out_of_range: false,
448            disallow_dynamic_selections: false,
449        }
450    }
451
452    // `eval()` should fail with all the same [`shape::ShapeError`]s
453    // as [`Shape::select()`].
454    #[allow(dead_code)]
455    pub fn strict() -> Self {
456        Self {
457            disallow_empty_ranges: true,
458            disallow_out_of_range: true,
459            ..Self::lenient()
460        }
461    }
462}
463
464impl Selection {
465    pub(crate) fn validate(&self, opts: &EvalOpts, slice: &Slice) -> Result<&Self, ShapeError> {
466        let depth = 0;
467        self.validate_rec(opts, slice, self, depth).map(|_| self)
468    }
469
470    fn validate_rec(
471        &self,
472        opts: &EvalOpts,
473        slice: &Slice,
474        top: &Selection,
475        dim: usize,
476    ) -> Result<(), ShapeError> {
477        if dim == slice.num_dim() {
478            // This enables us to maintain identities like 'all(true)
479            // <=> true' and 'all(false) <=> false' in leaf positions.
480            match self {
481                Selection::True | Selection::False => return Ok(()),
482                _ => {
483                    return Err(ShapeError::SelectionTooDeep {
484                        expr: top.clone(),
485                        num_dim: slice.num_dim(),
486                    });
487                }
488            }
489        }
490
491        match self {
492            Selection::False | Selection::True => Ok(()),
493            Selection::Range(range, s) => {
494                if range.is_empty() && opts.disallow_empty_ranges {
495                    return Err(ShapeError::EmptyRange {
496                        range: range.clone(),
497                    });
498                } else {
499                    if opts.disallow_out_of_range {
500                        let size = slice.sizes()[dim];
501                        let (min, _, _) = range.resolve(size);
502                        if min >= size {
503                            // Use EmptyRange here for now (evaluation would result in an empty range),
504                            // until we figure out how to differentiate between slices and shapes
505                            return Err(ShapeError::EmptyRange {
506                                range: range.clone(),
507                            });
508                        }
509                    }
510
511                    s.validate_rec(opts, slice, top, dim + 1)?;
512                }
513
514                Ok(())
515            }
516            Selection::Any(s) => {
517                if opts.disallow_dynamic_selections {
518                    return Err(ShapeError::SelectionDynamic { expr: top.clone() });
519                }
520                s.validate_rec(opts, slice, top, dim + 1)?;
521                Ok(())
522            }
523            Selection::All(s) | Selection::Label(_, s) | Selection::First(s) => {
524                s.validate_rec(opts, slice, top, dim + 1)?;
525                Ok(())
526            }
527            Selection::Intersection(a, b) | Selection::Union(a, b) => {
528                a.validate_rec(opts, slice, top, dim)?;
529                b.validate_rec(opts, slice, top, dim)?;
530                Ok(())
531            }
532        }
533    }
534
535    /// Lazily evaluates this selection against the given `slice`
536    /// yielding flat indices.
537    ///
538    /// Returns a boxed iterator that produces indices of elements
539    /// matching the selection expression when evaluated over `slice`.
540    ///
541    /// # Lifetimes
542    ///
543    /// The returned iterator borrows `slice` because the internal
544    /// iterators are implemented as closures that **capture**
545    /// `&slice` in their environment. Evaluation is lazy, so these
546    /// closures dereference `slice` each time a coordinate is
547    /// visited. The `'a` lifetime ensures that the iterator cannot
548    /// outlive the `slice` it reads from.
549    ///
550    /// # Why `Box<dyn Iterator>`
551    ///
552    /// The selection algebra supports multiple recursive strategies
553    /// (`All`, `Range`, `Intersection`, etc.) that return different
554    /// iterator types (e.g. `Selection::True` =>
555    /// `std::iter::once(...)`, `Selection::False` =>
556    /// `std::iter::empty()`). Returning `impl Iterator` is not
557    /// feasible because the precise type depends on dynamic selection
558    /// structure. Boxing erases this variability and allows a uniform
559    /// return type.
560    ///
561    /// # Canonical handling of 0-dimensional slices
562    ///
563    /// A `Slice` with zero dimensions represents the empty product
564    /// `∏_{i=1}^{0} Xᵢ`, which has exactly one element: the empty
565    /// tuple. To ensure that evaluation behaves uniformly across
566    /// dimensions, we canonically embed the 0-dimensional case into a
567    /// 1-dimensional slice of extent 1. That is, we reinterpret the
568    /// 0D slice as `Slice::new(offset, [1], [1])`, which is
569    /// semantically equivalent and enables evaluation to proceed
570    /// through the normal recursive machinery without special-casing.
571    /// The result is that selection expressions are always evaluated
572    /// over a slice with at least one dimension, and uniform logic
573    /// applies.
574    pub fn eval<'a>(
575        &self,
576        opts: &EvalOpts,
577        slice: &'a Slice,
578    ) -> Result<Box<dyn Iterator<Item = usize> + 'a>, ShapeError> {
579        // Canonically embed 0D as 1D (extent 1).
580        if slice.num_dim() == 0 {
581            let slice = Slice::new(slice.offset(), vec![1], vec![1]).unwrap();
582            return Ok(Box::new(
583                self.validate(opts, &slice)?
584                    .eval_rec(&slice, vec![0; 1], 0)
585                    .collect::<Vec<_>>()
586                    .into_iter(),
587            ));
588        }
589
590        Ok(self
591            .validate(opts, slice)?
592            .eval_rec(slice, vec![0; slice.num_dim()], 0))
593    }
594
595    fn eval_rec<'a>(
596        &self,
597        slice: &'a Slice,
598        env: Vec<usize>,
599        dim: usize,
600    ) -> Box<dyn Iterator<Item = usize> + 'a> {
601        if dim == slice.num_dim() {
602            match self {
603                Selection::True => return Box::new(std::iter::once(slice.location(&env).unwrap())),
604                Selection::False => return Box::new(std::iter::empty()),
605                _ => {
606                    panic!("structural combinator {self:?} at leaf level (dim = {dim}))",);
607                }
608            }
609        }
610
611        use itertools;
612        use itertools::EitherOrBoth;
613
614        match self {
615            Selection::False => Box::new(std::iter::empty()),
616            Selection::True => Box::new((0..slice.sizes()[dim]).flat_map(move |i| {
617                let mut env = env.clone();
618                env[dim] = i;
619                Selection::True.eval_rec(slice, env, dim + 1)
620            })),
621            Selection::All(select) => {
622                let select = Box::clone(select);
623                Box::new((0..slice.sizes()[dim]).flat_map(move |i| {
624                    let mut env = env.clone();
625                    env[dim] = i;
626                    select.eval_rec(slice, env, dim + 1)
627                }))
628            }
629            Selection::First(select) => {
630                let select = Box::clone(select);
631                Box::new(iterutils::first(slice.sizes()[dim], move |i| {
632                    let mut env = env.clone();
633                    env[dim] = i;
634                    select.eval_rec(slice, env, dim + 1)
635                }))
636            }
637            Selection::Range(range, select) => {
638                let select = Box::clone(select);
639                let (min, max, step) = range.resolve(slice.sizes()[dim]);
640                Box::new((min..max).step_by(step).flat_map(move |i| {
641                    let mut env = env.clone();
642                    env[dim] = i;
643                    select.eval_rec(slice, env, dim + 1)
644                }))
645            }
646
647            // Label-based selection: filters candidates at this
648            // dimension, then either selects one (Any) or recurses.
649            //
650            // When the inner selection is `Any`, we choose one match
651            // at random (eager). Otherwise, we recurse normally and
652            // filter the results lazily.
653            //
654            // This separation reflects that `Label(...)` does *not*
655            // consume a dimension — it restricts access to it while
656            // preserving dimensional structure.
657            //
658            // See `eval_label` for more on the distinction between
659            // filtering and traversal, and the underlying
660            // projection-based interpretation.
661            //
662            // For example:
663            //
664            //   sel!(*, ["foo"]?, *)  // select one host with label "foo", then all GPUs
665            //   = all(label(["foo"], any(all(true_()))))
666            //
667            //   sel!(*, ["foo"]*, *)  // select all hosts with label "foo", then all GPUs
668            //   = all(label(["foo"], all(all(true_()))))
669            //
670            // **Note:** Label filtering is not yet implemented — all coordinates
671            // are currently accepted.
672            Selection::Label(labels, inner) => {
673                Self::eval_label(labels, inner, slice, env, dim /*, provider */)
674            }
675            Selection::Any(select) => {
676                let select = Box::clone(select);
677                let r = {
678                    let upper = slice.sizes()[dim];
679                    let mut rng = rand::rng();
680                    rng.random_range(0..upper)
681                };
682                Box::new((r..r + 1).flat_map(move |i| {
683                    let mut env = env.clone();
684                    env[dim] = i;
685                    select.eval_rec(slice, env, dim + 1)
686                }))
687            }
688            Selection::Intersection(a, b) => Box::new(
689                itertools::merge_join_by(
690                    a.eval_rec(slice, env.clone(), dim),
691                    b.eval_rec(slice, env.clone(), dim),
692                    |x, y| x.cmp(y),
693                )
694                .filter_map(|either| match either {
695                    EitherOrBoth::Both(x, _) => Some(x),
696                    _ => None,
697                }),
698            ),
699            Selection::Union(a, b) => Box::new(
700                itertools::merge_join_by(
701                    a.eval_rec(slice, env.clone(), dim),
702                    b.eval_rec(slice, env.clone(), dim),
703                    |x, y| x.cmp(y),
704                )
705                .map(|either| match either {
706                    EitherOrBoth::Left(x) => x,
707                    EitherOrBoth::Right(y) => y,
708                    EitherOrBoth::Both(x, _) => x,
709                }),
710            ),
711        }
712    }
713
714    /// Evaluates a `Label(labels, inner)` selection.
715    ///
716    /// This operator filters coordinates along the current dimension
717    /// based on associated metadata (labels). It then evaluates the inner
718    /// selection at matching positions.
719    ///
720    /// Conceptually, this corresponds to computing a pullback along a
721    /// projection `p : E → B`, where:
722    ///
723    /// - `B` is the base space of coordinates (e.g. zones × hosts × gpus)
724    /// - `E` is the space of labeled coordinates
725    /// - `p⁻¹(S)` lifts a geometric selection `S ⊆ B` into the labeled
726    ///   space
727    ///
728    /// At runtime, we simulate `p⁻¹(S)` by traversing `B` and querying a
729    /// `LabelProvider` at each coordinate. Under the identity provider,
730    /// label filtering has no effect and `eval_label` reduces to the
731    /// geometric case.
732    ///
733    /// - If `inner` is `Any`, we select one matching index at random
734    /// - Otherwise, we recurse and filter lazily
735    ///
736    /// **Note:** Label filtering is not yet implemented — all coordinates
737    /// are currently accepted.
738    fn eval_label<'a>(
739        _labels: &[LabelKey],
740        inner: &Selection,
741        slice: &'a Slice,
742        env: Vec<usize>,
743        dim: usize,
744        // provider: &dyn LabelProvider  // TODO: add when ready
745    ) -> Box<dyn Iterator<Item = usize> + 'a> {
746        match inner {
747            // Case 1: label(..., any(...))
748            // - We evaluate all indices at this dimension that match
749            //   the label predicate.
750            // - From those, choose one at random and continue
751            //   evaluating the inner selection.
752            // - Semantically: filter → choose one → recurse
753            Selection::Any(sub_inner) => {
754                let matching: Vec<usize> = (0..slice.sizes()[dim])
755                    .filter(|&i| {
756                        let mut prefix = env.clone();
757                        prefix[dim] = i;
758                        true // TODO: provider.matches(dim, &prefix[0..=dim], labels)
759                    })
760                    .collect();
761
762                if matching.is_empty() {
763                    return Box::new(std::iter::empty());
764                }
765
766                let mut rng = rand::rng();
767                let chosen = matching[rng.random_range(0..matching.len())];
768
769                let mut coord = env;
770                coord[dim] = chosen;
771                sub_inner.eval_rec(slice, coord, dim + 1 /*, provider */)
772            }
773            // Case 2: label(..., inner)
774            //
775            // Applies label filtering after evaluating `inner`. We
776            // first recurse into `inner`, then lazily filter the
777            // resulting flat indices based on whether the coordinate
778            // at `dim` matches the given labels.
779            //
780            // This preserves laziness for all cases except `Any`,
781            // which requires eager collection and is handled
782            // separately.
783            _ => {
784                // evaluate the inner selection — recurse as usual
785                let iter = inner.eval_rec(slice, env.clone(), dim /* , provider */);
786                Box::new(iter.filter(move |&flat| {
787                    let _coord = slice.coordinates(flat);
788                    true // TODO: provider.matches(dim, &coord[0..=dim], labels)
789                }))
790            }
791        }
792    }
793
794    /// Returns `true` if this selection is equivalent to `True` under
795    /// the algebra.
796    ///
797    /// In the selection algebra, `All(True)` is considered equivalent
798    /// to `True`, and this identity extends recursively. For example:
799    ///
800    ///   - `All(True)`      ≡ `True`
801    ///   - `All(All(True))` ≡ `True`
802    ///   - `All(All(All(True)))` ≡ `True`
803    ///
804    /// This method checks whether the selection is structurally
805    /// identical to True, possibly wrapped in one or more All(...)
806    /// layers. It does **not** perform full normalization—only
807    /// structural matching sufficient to recognize this identity.
808    ///
809    /// Used to detect when a selection trivially selects all elements
810    /// at all levels.
811    ///
812    /// ## Limitations
813    ///
814    /// This is a **syntactic check** only. It does *not* recognize
815    /// semantically equivalent expressions such as:
816    ///
817    ///   - `Union(True, True)`
818    ///   - `All(Union(True, False))`
819    ///   - A union of all singleton ranges covering the full space
820    ///
821    /// For a semantic check, use evaluation against a known slice.
822    pub fn is_equivalent_to_true(mut sel: &Selection) -> bool {
823        while let Selection::All(inner) = sel {
824            sel = inner;
825        }
826        matches!(sel, Selection::True)
827    }
828
829    /// Evaluates whether the specified coordinates are part of the selection.
830    /// Returns true if they are, false otherwise.
831    ///
832    /// Example:
833    /// let selection = union(
834    ///     range(0..2, range(0..1, range(0..2, true_()))),
835    ///     range(0..2, range(1..2, range(0..2, true_()))),
836    /// );
837    ///
838    /// assert!(selection.contains(&[0, 0, 1]));
839    /// assert!(!selection.contains(&[2, 0, 1]));
840    pub fn contains(&self, coords: &[usize]) -> bool {
841        self.contains_rec(coords, 0)
842    }
843
844    fn contains_rec(&self, coords: &[usize], dim: usize) -> bool {
845        if dim >= coords.len() {
846            return matches!(self, Selection::True);
847        }
848
849        match self {
850            Selection::False => false,
851            Selection::True => true,
852            Selection::All(inner) => inner.contains_rec(coords, dim + 1),
853            Selection::Range(range, inner) => {
854                let (min, max, step) = range.resolve(coords.len());
855                let index = coords[dim];
856                index >= min
857                    && index < max
858                    && (index - min).is_multiple_of(step)
859                    && inner.contains_rec(coords, dim + 1)
860            }
861            Selection::Intersection(a, b) => {
862                a.contains_rec(coords, dim) && b.contains_rec(coords, dim)
863            }
864            Selection::Union(a, b) => a.contains_rec(coords, dim) || b.contains_rec(coords, dim),
865            Selection::Label(_, _) | Selection::First(_) | Selection::Any(_) => {
866                unimplemented!()
867            }
868        }
869    }
870
871    /// Simplifies the intersection of two `Selection` expressions.
872    ///
873    /// Applies short-circuit logic to avoid constructing redundant or
874    /// degenerate intersections:
875    ///
876    /// - If either side is `False`, the result is `False`.
877    /// - If either side is `True`, the result is the other side.
878    /// - Otherwise, constructs an explicit `Intersection`.
879    ///
880    /// This is required during routing to make progress when
881    /// evaluating intersections. Without this reduction, routing may
882    /// stall — for example, in intersections like `Intersection(True,
883    /// X)`, which should simplify to `X`.
884    pub fn reduce_intersection(self: Selection, b: Selection) -> Selection {
885        match (&self, &b) {
886            (Selection::False, _) | (_, Selection::False) => Selection::False,
887            (Selection::True, other) | (other, Selection::True) => other.clone(),
888            _ => Selection::Intersection(Box::new(self), Box::new(b)),
889        }
890    }
891
892    /// Recursively folds the `Selection` into an abstract syntax via
893    /// the `SelectionSYM` interface.
894    ///
895    /// This method structurally traverses the `Selection` tree and
896    /// reconstructs it using the operations provided by the
897    /// `SelectionSYM` trait. It is typically used to reify a
898    /// `Selection` into alternate forms, such as pretty-printers.
899    ///
900    /// # Type Parameters
901    ///
902    /// - `S`: An implementation of the `SelectionSYM` trait,
903    ///   providing the constructors for the target representation.
904    pub fn fold<S: SelectionSYM>(&self) -> S {
905        match self {
906            Selection::False => S::false_(),
907            Selection::True => S::true_(),
908            Selection::All(inner) => S::all(inner.fold::<S>()),
909            Selection::First(inner) => S::first(inner.fold::<S>()),
910            Selection::Range(r, inner) => S::range(r.clone(), inner.fold::<S>()),
911            Selection::Label(labels, inner) => S::label(labels.clone(), inner.fold::<S>()),
912            Selection::Any(inner) => S::any(inner.fold::<S>()),
913            Selection::Intersection(a, b) => S::intersection(a.fold::<S>(), b.fold::<S>()),
914            Selection::Union(a, b) => S::union(a.fold::<S>(), b.fold::<S>()),
915        }
916    }
917
918    /// Iterator over indices selected by `self` and not in
919    /// `exclusions`.
920    ///
921    /// Evaluates the selection against `slice` using `opts`, then
922    /// filters out any indices present in the exclusion set.
923    /// Evaluation is lazy and streaming; the exclusion set is used
924    /// directly for fast membership checks.
925    pub fn difference<'a>(
926        &self,
927        opts: &EvalOpts,
928        slice: &'a Slice,
929        exclusions: &'a HashSet<usize>,
930    ) -> Result<impl Iterator<Item = usize> + use<'a>, ShapeError> {
931        Ok(self
932            .eval(opts, slice)?
933            .filter(move |idx| !exclusions.contains(idx)))
934    }
935
936    /// Calculate a new `Selection` that excludes the specified flat
937    /// ranks.
938    ///
939    /// This computes `self \ exclusions` by evaluating `self`,
940    /// removing the given ranks, and reconstructing a `Selection`
941    /// that selects exactly the remaining elements.
942    ///
943    /// The result is a concrete, structurally uniform expression with
944    /// predictable construction order and exact correspondence to the
945    /// surviving ranks.
946    pub fn without(
947        &self,
948        slice: &Slice,
949        exclusions: &HashSet<usize>,
950    ) -> Result<Selection, ShapeError> {
951        let remaining = self
952            .difference(&EvalOpts::strict(), slice, exclusions)?
953            .collect::<BTreeSet<_>>();
954        Ok(Selection::of_ranks(slice, &remaining)?)
955    }
956
957    /// Converts a set of flat indices into a symbolic `Selection`
958    /// expression over the given `slice`. Returns an error if any index
959    /// is invalid.
960    ///
961    /// Each flat index is converted into coordinates using
962    /// `slice.coordinates`, then folded into a nested chain of singleton
963    /// ranges. The resulting selection evaluates exactly to the input
964    /// indices.
965    ///
966    /// The selections are combined left-associatively using `union`, but
967    /// since `union` is associative, the grouping does not affect
968    /// correctness.
969    ///
970    /// The input `BTreeSet` ensures:
971    /// - all indices are unique (no redundant singleton ranges),
972    /// - the resulting selection has a stable, deterministic structure,
973    /// - and iteration proceeds in ascending order, which helps produce
974    ///   predictable routing trees and consistent test results.
975    ///
976    /// This choice avoids an explicit sort and makes downstream behavior
977    /// more reproducible and auditable.
978    pub fn of_ranks(slice: &Slice, ranks: &BTreeSet<usize>) -> Result<Selection, SliceError> {
979        let selections = ranks
980            .iter()
981            .map(|&i| {
982                Ok(slice
983                    .coordinates(i)?
984                    .into_iter()
985                    .rev()
986                    .fold(dsl::true_(), |acc, i| dsl::range(i..=i, acc)))
987            })
988            .collect::<Result<Vec<_>, SliceError>>()?;
989
990        Ok(selections
991            .into_iter()
992            .reduce(dsl::union)
993            .unwrap_or_else(dsl::false_))
994    }
995} // impl Selection
996
997mod sealed {
998    pub trait Sealed {}
999    impl Sealed for crate::slice::Slice {}
1000}
1001
1002/// Connects the `select!` API to the `Selection` algebra by enabling
1003/// `base.reify_slice(slice)` syntax, where `base: Slice`.
1004///
1005/// The base slice defines the coordinate system in which the slice is
1006/// interpreted. Slices are themselves `Slice` values, typically
1007/// produced by `select!`, and are reified as `Selection` expressions
1008/// over the base.
1009pub trait ReifySlice: sealed::Sealed {
1010    /// Reify a slice as a `Selection` in the coordinate system of
1011    /// `self`.
1012    fn reify_slice(&self, slice: &Slice) -> Result<Selection, SliceError>;
1013
1014    /// Reify multiple slices as a union of selections in the
1015    /// coordinate system of `self`.
1016    fn reify_slices<V: AsRef<[Slice]>>(&self, slices: V) -> Result<Selection, SliceError>;
1017}
1018
1019impl ReifySlice for Slice {
1020    /// Constructs a [`Selection`] expression that symbolically
1021    /// matches all coordinates in the given `slice`, expressed in the
1022    /// coordinate system of the provided `base` slice (`self`).
1023    ///
1024    /// The result is a nested sequence of `range(start..end, step)`
1025    /// combinators that match the rectangular region covered by `slice`
1026    /// in base coordinates. This preserves geometry and layout when
1027    /// `slice` is *layout-aligned* — that is, each of its strides is
1028    /// a multiple of the corresponding base stride.
1029    ///
1030    /// If any dimension is not layout-aligned, the slice is reified
1031    /// by explicitly enumerating its coordinates.
1032    ///
1033    /// Returns [`dsl::false_()`] if the slice is empty.
1034    ///
1035    /// # Errors
1036    ///
1037    /// Returns an error if:
1038    /// - The base is not contiguous and row-major
1039    /// - The slice lies outside the bounds of the base
1040    ///
1041    /// # Example
1042    ///
1043    /// ```rust
1044    /// use ndslice::selection::ReifySlice;
1045    /// let shape = ndslice::shape!(x = 4, y = 4);
1046    /// let base = shape.slice();
1047    /// let selected = ndslice::select!(shape, x = 1..3, y = 2..4).unwrap();
1048    /// let slice = selected.slice();
1049    /// let selection = base.reify_slice(slice).unwrap();
1050    /// ```
1051    fn reify_slice(&self, slice: &Slice) -> Result<Selection, SliceError> {
1052        // Precondition: the base is contiguous and row major.
1053        if !self.is_contiguous() {
1054            return Err(SliceError::NonContiguous);
1055        }
1056
1057        if slice.is_empty() {
1058            return Ok(dsl::false_());
1059        }
1060
1061        if slice.num_dim() != self.num_dim()
1062            || slice.sizes().iter().zip(self.sizes()).any(|(&v, &s)| v > s)
1063        {
1064            return Selection::of_ranks(self, &slice.iter().collect::<BTreeSet<usize>>());
1065        }
1066
1067        let origin = self.coordinates(slice.offset())?;
1068        let mut acc = dsl::true_();
1069        for dim in (0..self.num_dim()).rev() {
1070            let start = origin[dim];
1071            let len = slice.sizes()[dim];
1072            let slice_stride = slice.strides()[dim];
1073            let base_stride = self.strides()[dim];
1074
1075            if slice_stride.is_multiple_of(base_stride) {
1076                // Layout-aligned with base.
1077                let step = slice_stride / base_stride;
1078                let end = start + step * len;
1079                // Check that `end` is within bounds.
1080                if end - 1 > self.sizes()[dim] {
1081                    let bad = origin
1082                        .iter()
1083                        .enumerate()
1084                        .map(|(d, &x)| if d == dim { end } else { x })
1085                        .collect::<Vec<_>>();
1086                    return Err(SliceError::ValueNotInSlice {
1087                        value: self.location(&bad).unwrap(),
1088                    });
1089                }
1090                acc = dsl::range(crate::shape::Range(start, Some(end), step), acc);
1091            } else {
1092                // Irregular layout; fallback to explicit enumeration.
1093                return Selection::of_ranks(self, &slice.iter().collect::<BTreeSet<_>>());
1094            }
1095        }
1096
1097        Ok(acc)
1098    }
1099
1100    /// Converts a list of `slices` into a symbolic [`Selection`]
1101    /// expression over a common `base` [`Slice`].
1102    ///
1103    /// Each slice describes a rectangular subregion of the base. This
1104    /// function reifies each slice into a nested `range(.., ..)`
1105    /// expression in the base coordinate system and returns the union
1106    /// of all such selections.
1107    ///
1108    /// Empty slices are ignored.
1109    ///
1110    /// # Errors
1111    ///
1112    /// Returns an error if any slice:
1113    /// - Refers to coordinates not contained within the base
1114    ///
1115    /// # Example
1116    ///
1117    /// ```rust
1118    /// use ndslice::selection::ReifySlice;
1119    ///
1120    /// let shape = ndslice::shape!(x = 4, y = 4);
1121    /// let base = shape.slice();
1122    ///
1123    /// let a = ndslice::select!(shape, x = 0..2, y = 0..2)
1124    ///     .unwrap()
1125    ///     .slice()
1126    ///     .clone();
1127    /// let b = ndslice::select!(shape, x = 2..4, y = 2..4)
1128    ///     .unwrap()
1129    ///     .slice()
1130    ///     .clone();
1131    ///
1132    /// let sel = base.reify_slices(&[a, b]).unwrap();
1133    /// ```
1134    fn reify_slices<V: AsRef<[Slice]>>(&self, slices: V) -> Result<Selection, SliceError> {
1135        let slices = slices.as_ref();
1136        let mut selections = Vec::with_capacity(slices.len());
1137
1138        for slice in slices {
1139            if slice.is_empty() {
1140                continue;
1141            }
1142            selections.push(self.reify_slice(slice)?);
1143        }
1144
1145        let mut iter = selections.into_iter();
1146        let first = iter.next().unwrap_or_else(dsl::false_);
1147        Ok(iter.fold(first, dsl::union))
1148    }
1149}
1150
1151/// Trivial all(true) equivalence.
1152pub fn is_equivalent_true(sel: impl std::borrow::Borrow<Selection>) -> bool {
1153    Selection::is_equivalent_to_true(sel.borrow())
1154}
1155
1156mod iterutils {
1157    // An iterator over the first non-empty result 1 applying
1158    // `mk_iter` to indices in the range `0..size`.
1159    pub(crate) fn first<'a, F>(size: usize, mut mk_iter: F) -> impl Iterator<Item = usize> + 'a
1160    where
1161        F: FnMut(usize) -> Box<dyn Iterator<Item = usize> + 'a>,
1162    {
1163        (0..size)
1164            .find_map(move |i| {
1165                let mut iter = mk_iter(i).peekable();
1166                if iter.peek().is_some() {
1167                    Some(iter)
1168                } else {
1169                    None
1170                }
1171            })
1172            .into_iter()
1173            .flatten()
1174    }
1175}
1176
1177/// Construct a [`Selection`] from a [`Shape`] and a single labeled
1178/// constraint.
1179///
1180/// This function produces a multidimensional selection expression
1181/// that is structurally aligned with the shape. It applies the given
1182/// range to the named dimension, and fills all preceding dimensions
1183/// with [`all`] to maintain alignment. Trailing dimensions are left
1184/// unconstrained.
1185///
1186/// # Arguments
1187///
1188/// - `shape`: The labeled shape describing the coordinate space.
1189/// - `label`: The name of the dimension to constrain.
1190/// - `rng`: The range to apply in the selected dimension.
1191///
1192/// # Errors
1193///
1194/// Returns [`ShapeError::InvalidLabels`] if the label is not present
1195/// in the shape.
1196///
1197/// # Example
1198///
1199/// ```
1200/// use ndslice::shape;
1201/// use ndslice::selection::selection_from_one;
1202///
1203/// let shape = shape!(zone = 2, host = 4, gpu = 8);
1204/// let sel = selection_from_one(&shape, "host", 1..3).unwrap();
1205/// assert_eq!(format!("{sel:?}"), "All(Range(Range(1, Some(3), 1), True))"); // corresponds to (*, 1..3, *)
1206/// ```
1207///
1208/// [`all`]: crate::selection::dsl::all
1209/// [`Shape`]: crate::shape::Shape
1210/// [`Selection`]: crate::selection::Selection
1211pub fn selection_from_one<R>(
1212    shape: &shape::Shape,
1213    label: &str,
1214    rng: R,
1215) -> Result<Selection, ShapeError>
1216where
1217    R: Into<shape::Range>,
1218{
1219    use crate::selection::dsl;
1220
1221    let Some(pos) = shape.labels().iter().position(|l| l == label) else {
1222        return Err(ShapeError::InvalidLabels {
1223            labels: vec![label.to_string()],
1224        });
1225    };
1226
1227    let mut selection = dsl::range(rng.into(), dsl::true_());
1228    for _ in 0..pos {
1229        selection = dsl::all(selection)
1230    }
1231
1232    Ok(selection)
1233}
1234
1235/// Construct a [`Selection`] from a [`Shape`] and multiple labeled
1236/// range constraints.
1237///
1238/// This function produces a multidimensional selection aligned with
1239/// the shape, applying the specified constraints to their
1240/// corresponding dimensions. All unconstrained dimensions are filled
1241/// with [`all`] to preserve structural alignment.
1242///
1243/// # Arguments
1244///
1245/// - `shape`: The labeled shape defining the full coordinate space.
1246/// - `constraints`: A slice of `(label, range)` pairs specifying
1247///   dimension constraints.
1248///
1249/// # Errors
1250///
1251/// Returns [`ShapeError::InvalidLabels`] if any label in
1252/// `constraints` is not present in the shape.
1253///
1254/// # Example
1255///
1256/// ```
1257/// use ndslice::selection::selection_from;
1258/// use ndslice::shape;
1259///
1260/// let shape = shape!(zone = 2, host = 4, gpu = 8);
1261/// let sel = selection_from(&shape, &[("host", 1..3), ("gpu", 0..4)]).unwrap();
1262/// assert_eq!(
1263///     format!("{sel:?}"),
1264///     "All(Range(Range(1, Some(3), 1), Range(Range(0, Some(4), 1), True)))"
1265/// );
1266/// ```
1267///
1268/// [`Shape`]: crate::shape::Shape
1269/// [`Selection`]: crate::selection::Selection
1270/// [`all`]: crate::selection::dsl::all
1271pub fn selection_from<'a, R>(
1272    shape: &shape::Shape,
1273    constraints: &[(&'a str, R)],
1274) -> Result<Selection, ShapeError>
1275where
1276    R: Clone + Into<shape::Range> + 'a,
1277{
1278    use crate::selection::dsl::*;
1279
1280    let mut label_to_constraint = HashMap::new();
1281    for (label, r) in constraints {
1282        if !shape.labels().iter().any(|l| l == label) {
1283            return Err(ShapeError::InvalidLabels {
1284                labels: vec![label.to_string()],
1285            });
1286        }
1287        label_to_constraint.insert(*label, r.clone().into());
1288    }
1289
1290    let selection = shape.labels().iter().rev().fold(true_(), |acc, label| {
1291        if let Some(rng) = label_to_constraint.get(label.as_str()) {
1292            range(rng.clone(), acc)
1293        } else {
1294            all(acc)
1295        }
1296    });
1297
1298    Ok(selection)
1299}
1300
1301/// Construct a [`Selection`] from a [`Shape`] using labeled dimension
1302/// constraints.
1303///
1304/// This macro provides a convenient syntax for specifying
1305/// sub-selections on a shape by labeling dimensions and applying
1306/// either exact indices or ranges. Internally, it wraps
1307/// [`selection_from_one`] and [`selection_from`] to produce a
1308/// fully-aligned [`Selection`] expression.
1309///
1310/// # Forms
1311///
1312/// - Single labeled range:
1313///   ```
1314///   let shape = ndslice::shape!(zone = 2, host = 4, gpu = 8);
1315///   let sel = ndslice::sel_from_shape!(&shape, host = 1..3);
1316///   ```
1317///
1318/// - Multiple exact indices (converted to `n..n+1`):
1319///   ```
1320///   let shape = ndslice::shape!(zone = 2, host = 4, gpu = 8);
1321///   let sel = ndslice::sel_from_shape!(&shape, zone = 1, gpu = 4);
1322///   ```
1323///
1324/// - Multiple labeled ranges:
1325///   ```
1326///   let shape = ndslice::shape!(zone = 2, host = 4, gpu = 8);
1327///   let sel = ndslice::sel_from_shape!(&shape, zone = 0..1, host = 1..3, gpu = 4..8);
1328///   ```
1329///
1330/// # Panics
1331///
1332/// This macro calls `.unwrap()` on the result of the underlying
1333/// functions. It will panic if any label is not found in the shape.
1334///
1335/// # See Also
1336///
1337/// - [`selection_from_one`]
1338/// - [`selection_from`]
1339/// - [`Selection`]
1340/// - [`Shape`]
1341///
1342/// [`Selection`]: crate::selection::Selection
1343/// [`Shape`]: crate::shape::Shape
1344/// [`selection_from_one`]: crate::selection::selection_from_one
1345/// [`selection_from`]: crate::selection::selection_from
1346#[macro_export]
1347macro_rules! sel_from_shape {
1348    ($shape:expr, $label:ident = $range:expr) => {
1349        $crate::selection::selection_from_one($shape, stringify!($label), $range).unwrap()
1350    };
1351
1352    ($shape:expr, $($label:ident = $val:literal),* $(,)?) => {
1353        $crate::selection::selection_from($shape,
1354                                          &[
1355                                              $((stringify!($label), $val..$val+1)),*
1356                                          ]).unwrap()
1357    };
1358
1359    ($shape:expr, $($label:ident = $range:expr),* $(,)?) => {
1360        $crate::selection::selection_from($shape, &[
1361            $((stringify!($label), $range)),*
1362        ]).unwrap()
1363    };
1364}
1365
1366#[cfg(test)]
1367mod tests {
1368    use std::assert_matches;
1369    use std::collections::BTreeSet;
1370
1371    use super::EvalOpts;
1372    use super::ReifySlice;
1373    use super::Selection;
1374    use super::dsl::*;
1375    use super::is_equivalent_true;
1376    use crate::Range;
1377    use crate::Slice;
1378    use crate::assert_structurally_eq;
1379    use crate::select;
1380    use crate::shape;
1381    use crate::shape::ShapeError;
1382
1383    // A test slice: (zones = 2, hosts = 4, gpus = 8).
1384    fn test_slice() -> Slice {
1385        Slice::new(0usize, vec![2, 4, 8], vec![32, 8, 1]).unwrap()
1386    }
1387
1388    // Given expression `expr`, options `opts` and slice `slice`,
1389    // canonical usage is:
1390    // ```rust
1391    // let nodes = expr.eval(&opts, slice.clone())?.collect::<Vec<usize>>();
1392    // ```
1393    // This utility cuts down on the syntactic repetition that results
1394    // from the above in the tests that follow.
1395    fn eval(expr: Selection, slice: &Slice) -> Vec<usize> {
1396        expr.eval(&EvalOpts::lenient(), slice).unwrap().collect()
1397    }
1398
1399    #[test]
1400    fn test_selection_00() {
1401        let slice = &test_slice();
1402
1403        // No GPUs on any host in any region.
1404        assert!(eval(false_(), slice).is_empty());
1405        assert!(eval(all(false_()), slice).is_empty());
1406        assert!(eval(all(all(false_())), slice).is_empty());
1407
1408        // All GPUs on all hosts in all regions.
1409        assert_eq!((0..=63).collect::<Vec<_>>(), eval(true_(), slice));
1410        assert_eq!(eval(true_(), slice), eval(all(true_()), slice));
1411        assert_eq!(eval(all(true_()), slice), eval(all(all(true_())), slice));
1412
1413        // Terminal `true_()` and `false_()` selections are allowed at
1414        // the leaf.
1415        assert_eq!(eval(true_(), slice), eval(all(all(all(true_()))), slice));
1416        assert!(eval(all(all(all(false_()))), slice).is_empty());
1417    }
1418
1419    #[test]
1420    fn test_selection_01() {
1421        let slice = &test_slice();
1422
1423        // Structural combinators beyond the slice's dimensionality
1424        // are invalid.
1425        let expr = all(all(all(all(true_()))));
1426        let result = expr.validate(&EvalOpts::lenient(), slice);
1427        assert!(
1428            matches!(result, Err(ShapeError::SelectionTooDeep { .. })),
1429            "Unexpected: {:?}",
1430            result
1431        );
1432        assert_eq!(
1433            format!("{}", result.unwrap_err()),
1434            "selection `all(all(all(all(true_()))))` exceeds dimensionality 3"
1435        );
1436    }
1437
1438    #[test]
1439    fn test_selection_02() {
1440        let slice = &test_slice();
1441
1442        // GPU 0 on host 0 in region 0.
1443        let select = range(0..=0, range(0..=0, range(0..=0, true_())));
1444        assert_eq!((0..=0).collect::<Vec<_>>(), eval(select, slice));
1445
1446        // GPU 1 on host 1 in region 1.
1447        let select = range(1..=1, range(1..=1, range(1..=1, true_())));
1448        assert_eq!((41..=41).collect::<Vec<_>>(), eval(select, slice));
1449
1450        // All GPUs on host 0 in all regions:
1451        let select = all(range(0..=0, all(true_())));
1452        assert_eq!(
1453            (0..=7).chain(32..=39).collect::<Vec<_>>(),
1454            eval(select, slice)
1455        );
1456
1457        // All GPUs on host 1 in all regions:
1458        let select = all(range(1..=1, all(true_())));
1459        assert_eq!(
1460            (8..=15).chain(40..=47).collect::<Vec<_>>(),
1461            eval(select, slice)
1462        );
1463
1464        // The first 4 GPUs on all hosts in all regions:
1465        let select = all(all(range(0..4, true_())));
1466        assert_eq!(
1467            (0..=3)
1468                .chain(8..=11)
1469                .chain(16..=19)
1470                .chain(24..=27)
1471                .chain(32..=35)
1472                .chain(40..=43)
1473                .chain(48..=51)
1474                .chain(56..=59)
1475                .collect::<Vec<_>>(),
1476            eval(select, slice)
1477        );
1478
1479        // The last 4 GPUs on all hosts in all regions:
1480        let select = all(all(range(4..8, true_())));
1481        assert_eq!(
1482            (4..=7)
1483                .chain(12..=15)
1484                .chain(20..=23)
1485                .chain(28..=31)
1486                .chain(36..=39)
1487                .chain(44..=47)
1488                .chain(52..=55)
1489                .chain(60..=63)
1490                .collect::<Vec<_>>(),
1491            eval(select, slice)
1492        );
1493
1494        // All regions, all hosts, odd GPUs:
1495        let select = all(all(range(shape::Range(1, None, 2), true_())));
1496        assert_eq!(
1497            (1..8)
1498                .step_by(2)
1499                .chain((9..16).step_by(2))
1500                .chain((17..24).step_by(2))
1501                .chain((25..32).step_by(2))
1502                .chain((33..40).step_by(2))
1503                .chain((41..48).step_by(2))
1504                .chain((49..56).step_by(2))
1505                .chain((57..64).step_by(2))
1506                .collect::<Vec<_>>(),
1507            eval(select, slice)
1508        );
1509    }
1510
1511    #[test]
1512    fn test_selection_03() {
1513        let slice = &test_slice();
1514
1515        assert_eq!(
1516            eval(intersection(true_(), true_()), slice),
1517            eval(true_(), slice)
1518        );
1519        assert_eq!(
1520            eval(intersection(true_(), false_()), slice),
1521            eval(false_(), slice)
1522        );
1523        assert_eq!(
1524            eval(intersection(false_(), true_()), slice),
1525            eval(false_(), slice)
1526        );
1527        assert_eq!(
1528            eval(intersection(false_(), false_()), slice),
1529            eval(false_(), slice)
1530        );
1531        assert_eq!(
1532            eval(
1533                intersection(
1534                    all(all(range(0..=3, true_()))),
1535                    all(all(range(4..=7, true_())))
1536                ),
1537                slice
1538            ),
1539            eval(false_(), slice)
1540        );
1541        assert_eq!(
1542            eval(intersection(true_(), all(all(range(4..8, true_())))), slice),
1543            eval(all(all(range(4..8, true_()))), slice)
1544        );
1545        assert_eq!(
1546            eval(
1547                intersection(
1548                    all(all(range(0..=4, true_()))),
1549                    all(all(range(4..=7, true_())))
1550                ),
1551                slice
1552            ),
1553            eval(all(all(range(4..=4, true_()))), slice)
1554        );
1555    }
1556
1557    #[test]
1558    fn test_selection_04() {
1559        let slice = &test_slice();
1560
1561        assert_eq!(eval(union(true_(), true_()), slice), eval(true_(), slice));
1562        assert_eq!(eval(union(false_(), true_()), slice), eval(true_(), slice));
1563        assert_eq!(eval(union(true_(), false_()), slice), eval(true_(), slice));
1564        assert_eq!(
1565            eval(union(false_(), false_()), slice),
1566            eval(false_(), slice)
1567        );
1568        assert_eq!(
1569            eval(
1570                union(
1571                    all(all(range(0..4, true_()))),
1572                    all(all(range(4.., true_())))
1573                ),
1574                slice
1575            ),
1576            eval(true_(), slice)
1577        );
1578
1579        // Across all regions, get the first 4 GPUs on host 0 and the
1580        // last 4 GPUs on host 1.
1581        let s = all(range(0..=0, range(0..4, true_())));
1582        let t = all(range(1..=1, range(4.., true_())));
1583        assert_eq!(
1584            (0..=3)
1585                .chain(12..=15)
1586                .chain(32..=35)
1587                .chain(44..=47)
1588                .collect::<Vec<_>>(),
1589            eval(union(s, t), slice)
1590        );
1591
1592        // All regions, all hosts, skip GPUs 2, 3, 4 and 5.
1593        assert_eq!(
1594            // z=0, h=0
1595            (0..=1)
1596                .chain(6..=7)
1597                // z=0, h=1
1598                .chain(8..=9)
1599                .chain(14..=15)
1600                // z=0, h=2
1601                .chain(16..=17)
1602                .chain(22..=23)
1603                // z=0, h=3
1604                .chain(24..=25)
1605                .chain(30..=31)
1606                // z=1, h=0
1607                .chain(32..=33)
1608                .chain(38..=39)
1609                // z=1, h=1
1610                .chain(40..=41)
1611                .chain(46..=47)
1612                // z=1, h=2
1613                .chain(48..=49)
1614                .chain(54..=55)
1615                // z=1, h=3
1616                .chain(56..=57)
1617                .chain(62..=63)
1618                .collect::<Vec<_>>(),
1619            eval(
1620                all(all(union(range(0..2, true_()), range(6..8, true_())))),
1621                slice
1622            )
1623        );
1624
1625        // All regions, all hosts, odd GPUs.
1626        assert_eq!(
1627            (1..8)
1628                .step_by(2)
1629                .chain((9..16).step_by(2))
1630                .chain((17..24).step_by(2))
1631                .chain((25..32).step_by(2))
1632                .chain((33..40).step_by(2))
1633                .chain((41..48).step_by(2))
1634                .chain((49..56).step_by(2))
1635                .chain((57..64).step_by(2))
1636                .collect::<Vec<_>>(),
1637            eval(
1638                all(all(union(
1639                    range(shape::Range(1, Some(4), 2), true_()),
1640                    range(shape::Range(5, Some(8), 2), true_())
1641                ))),
1642                slice
1643            )
1644        );
1645        assert_eq!(
1646            eval(
1647                all(all(union(
1648                    range(shape::Range(1, Some(4), 2), true_()),
1649                    range(shape::Range(5, Some(8), 2), true_())
1650                ))),
1651                slice
1652            ),
1653            eval(
1654                all(all(union(
1655                    union(range(1..=1, true_()), range(3..=3, true_()),),
1656                    union(range(5..=5, true_()), range(7..=7, true_()),),
1657                ))),
1658                slice
1659            ),
1660        );
1661    }
1662
1663    #[test]
1664    fn test_selection_05() {
1665        let slice = &test_slice();
1666
1667        // First region, first host, no GPU.
1668        assert!(eval(first(first(false_())), slice).is_empty());
1669        // First region, first host, first GPU.
1670        assert_eq!(vec![0], eval(first(first(range(0..1, true_()))), slice));
1671        // First region, first host, all GPUs.
1672        assert_eq!(
1673            (0..8).collect::<Vec<_>>(),
1674            eval(first(first(true_())), slice)
1675        );
1676
1677        // Terminal `true_()` and `false_()` selections are allowed at
1678        // the leaf.
1679        // First region, first host, no GPU.
1680        assert!(eval(first(first(first(false_()))), slice).is_empty());
1681        // First region, first host, first GPU.
1682        assert_eq!(vec![0], eval(first(first(first(true_()))), slice));
1683
1684        // All regions, first host, all GPUs.
1685        assert_eq!(
1686            (0..8).chain(32..40).collect::<Vec<_>>(),
1687            eval(all(first(true_())), slice)
1688        );
1689
1690        // First region, first host, GPUs 0, 1 and 2.
1691        assert_eq!(
1692            (0..3).collect::<Vec<_>>(),
1693            eval(first(first(range(0..=2, true_()))), slice)
1694        );
1695    }
1696
1697    #[test]
1698    fn test_selection_06() {
1699        let slice = &test_slice();
1700
1701        // Structural combinators beyond the slice's dimensionality
1702        // are invalid.
1703        let expr = first(first(first(first(true_()))));
1704        let result = expr.validate(&EvalOpts::lenient(), slice);
1705        assert!(
1706            matches!(result, Err(ShapeError::SelectionTooDeep { .. })),
1707            "Unexpected: {:?}",
1708            result
1709        );
1710        assert_eq!(
1711            format!("{}", result.unwrap_err()),
1712            "selection `first(first(first(first(true_()))))` exceeds dimensionality 3"
1713        );
1714    }
1715
1716    #[test]
1717    fn test_selection_07() {
1718        use crate::select;
1719        use crate::shape;
1720
1721        // 2 x 8 row-major.
1722        let s = shape!(host = 2, gpu = 8);
1723
1724        // All GPUs on host 1.
1725        assert_eq!(
1726            select!(s, host = 1)
1727                .unwrap()
1728                .slice()
1729                .iter()
1730                .collect::<Vec<_>>(),
1731            eval(range(1..2, true_()), s.slice())
1732        );
1733
1734        // All hosts, GPUs 2 through 7.
1735        assert_eq!(
1736            select!(s, gpu = 2..)
1737                .unwrap()
1738                .slice()
1739                .iter()
1740                .collect::<Vec<_>>(),
1741            eval(all(range(2.., true_())), s.slice())
1742        );
1743
1744        // All hosts, GPUs 3 and 4.
1745        assert_eq!(
1746            select!(s, gpu = 3..5)
1747                .unwrap()
1748                .slice()
1749                .iter()
1750                .collect::<Vec<_>>(),
1751            eval(all(range(3..5, true_())), s.slice())
1752        );
1753
1754        // GPUS 3 and 4 on host 1.
1755        assert_eq!(
1756            select!(s, gpu = 3..5, host = 1)
1757                .unwrap()
1758                .slice()
1759                .iter()
1760                .collect::<Vec<_>>(),
1761            eval(range(1..=1, range(3..5, true_())), s.slice())
1762        );
1763
1764        // All hosts, no GPUs.
1765        assert_matches!(
1766            select!(s, gpu = 1..1).unwrap_err(),
1767            ShapeError::EmptyRange {
1768                range: shape::Range(1, Some(1), 1)
1769            },
1770        );
1771        assert!(eval(all(range(1..1, true_())), s.slice()).is_empty());
1772
1773        // All hosts, GPU 8.
1774        assert_matches!(
1775            select!(s, gpu = 8).unwrap_err(),
1776            ShapeError::OutOfRange {
1777                range: shape::Range(8, Some(9), 1),
1778                dim,
1779                size: 8,
1780            } if dim == "gpu",
1781        );
1782        assert!(eval(all(range(8..8, true_())), s.slice()).is_empty());
1783    }
1784
1785    #[test]
1786    fn test_selection_08() {
1787        let s = &shape!(host = 2, gpu = 8);
1788
1789        assert_eq!(
1790            eval(range(1..2, true_()), s.slice()),
1791            eval(sel_from_shape!(s, host = 1), s.slice())
1792        );
1793
1794        assert_eq!(
1795            eval(all(range(2.., true_())), s.slice()),
1796            eval(sel_from_shape!(s, gpu = 2..), s.slice())
1797        );
1798
1799        assert_eq!(
1800            eval(all(range(3..5, true_())), s.slice()),
1801            eval(sel_from_shape!(s, gpu = 3..5), s.slice())
1802        );
1803
1804        assert_eq!(
1805            eval(range(1..2, range(3..5, true_())), s.slice()),
1806            eval(sel_from_shape!(s, host = 1..2, gpu = 3..5), s.slice())
1807        );
1808
1809        assert_eq!(
1810            eval(
1811                union(
1812                    sel_from_shape!(s, host = 0..1, gpu = 0..4),
1813                    sel_from_shape!(s, host = 1..2, gpu = 4..5)
1814                ),
1815                s.slice()
1816            ),
1817            eval(
1818                union(
1819                    range(0..1, range(0..4, true_())),
1820                    range(1..2, range(4..5, true_()))
1821                ),
1822                s.slice()
1823            )
1824        );
1825    }
1826
1827    #[test]
1828    fn test_selection_09() {
1829        let slice = &test_slice(); // 2 x 4 x 8
1830
1831        // Identity.
1832        assert_eq!(eval(any(false_()), slice), eval(false_(), slice));
1833
1834        // An arbitrary GPU.
1835        let res = eval(any(any(any(true_()))), slice);
1836        assert_eq!(res.len(), 1);
1837        assert!(res[0] < 64);
1838
1839        // The first 4 GPUs of any host in region-0.
1840        let res = eval(range(0, any(range(0..4, true_()))), slice);
1841        assert!((0..4).any(|host| res == eval(range(0, range(host, range(0..4, true_()))), slice)));
1842
1843        // Any GPU on host-0 in region-0.
1844        let res = eval(range(0, range(0, any(true_()))), slice);
1845        assert_eq!(res.len(), 1);
1846        assert!(res[0] < 8);
1847
1848        // All GPUs on any host in region-0.
1849        let res = eval(range(0, any(true_())), slice);
1850        assert!((0..4).any(|host| res == eval(range(0, range(host, true_())), slice)));
1851
1852        // All GPUs on any host in region-1.
1853        let res = eval(range(1, any(true_())), slice);
1854        assert!((0..4).any(|host| res == eval(range(1, range(host, true_())), slice)));
1855
1856        // Any two GPUs on host-0 in region-0.
1857        let mut res = vec![];
1858        while res.len() < 2 {
1859            res = eval(
1860                union(
1861                    range(0, range(0, any(true_()))),
1862                    range(0, range(0, any(true_()))),
1863                ),
1864                slice,
1865            );
1866        }
1867        assert_matches!(res.as_slice(), [i, j] if *i < *j && *i < 8 && *j < 8);
1868    }
1869
1870    #[test]
1871    fn test_eval_zero_dim_slice() {
1872        let slice_0d = Slice::new(1, vec![], vec![]).unwrap();
1873        // Let s be a slice with dim(s) = 0. Then: ∃! x ∈ s :
1874        // coordsₛ(x) = ().
1875        assert_eq!(slice_0d.coordinates(1).unwrap(), Vec::<usize>::new());
1876
1877        assert_eq!(eval(true_(), &slice_0d), vec![1]);
1878        assert_eq!(eval(false_(), &slice_0d), Vec::<usize>::new());
1879        assert_eq!(eval(all(true_()), &slice_0d), vec![1]);
1880        assert_eq!(eval(all(false_()), &slice_0d), Vec::<usize>::new());
1881        assert_eq!(eval(union(true_(), true_()), &slice_0d), vec![1]);
1882        assert_eq!(
1883            eval(intersection(true_(), false_()), &slice_0d),
1884            Vec::<usize>::new()
1885        );
1886    }
1887
1888    #[test]
1889    fn test_selection_10() {
1890        let slice = &test_slice();
1891        let opts = EvalOpts {
1892            disallow_dynamic_selections: true,
1893            ..EvalOpts::lenient()
1894        };
1895        let expr = any(any(any(true_())));
1896        let res = expr.validate(&opts, slice);
1897        assert_matches!(res, Err(ShapeError::SelectionDynamic { .. }));
1898    }
1899
1900    #[test]
1901    fn test_13() {
1902        // Structural identity: `all(true)` <=> `true`.
1903        assert!(is_equivalent_true(true_()));
1904        assert!(is_equivalent_true(all(true_())));
1905        assert!(is_equivalent_true(all(all(true_()))));
1906        assert!(is_equivalent_true(all(all(all(true_())))));
1907        assert!(is_equivalent_true(all(all(all(all(true_()))))));
1908        assert!(is_equivalent_true(all(all(all(all(all(true_())))))));
1909        // ...
1910
1911        assert!(!is_equivalent_true(false_()));
1912        assert!(!is_equivalent_true(union(true_(), true_())));
1913        assert!(!is_equivalent_true(range(0..=0, true_())));
1914        assert!(!is_equivalent_true(all(false_())));
1915    }
1916
1917    #[test]
1918    fn test_14() {
1919        use std::collections::HashSet;
1920
1921        use crate::selection::NormalizedSelectionKey;
1922        use crate::selection::dsl::*;
1923
1924        let a = all(all(true_()));
1925        let b = all(all(true_()));
1926
1927        let key_a = NormalizedSelectionKey::new(&a);
1928        let key_b = NormalizedSelectionKey::new(&b);
1929
1930        // They should be structurally equal.
1931        assert_eq!(key_a, key_b);
1932
1933        // Their hashes should agree, and they deduplicate in a set.
1934        let mut set = HashSet::new();
1935        set.insert(key_a);
1936        assert!(set.contains(&key_b));
1937    }
1938
1939    #[test]
1940    fn test_contains_true() {
1941        let selection = true_();
1942        assert!(selection.contains(&[0, 0, 0]));
1943        assert!(selection.contains(&[1, 2, 3]));
1944    }
1945
1946    #[test]
1947    fn test_contains_false() {
1948        let selection = false_();
1949        assert!(!selection.contains(&[0, 0, 0]));
1950        assert!(!selection.contains(&[1, 2, 3]));
1951    }
1952
1953    #[test]
1954    fn test_contains_all() {
1955        let selection = all(true_());
1956        assert!(selection.contains(&[0, 0, 0]));
1957        assert!(selection.contains(&[1, 2, 3]));
1958    }
1959
1960    #[test]
1961    fn test_contains_range() {
1962        let selection = range(1..3, true_());
1963        assert!(selection.contains(&[1, 0, 0]));
1964        assert!(!selection.contains(&[3, 0, 0]));
1965    }
1966
1967    #[test]
1968    fn test_contains_intersection() {
1969        let selection = intersection(range(1..3, true_()), range(2..4, true_()));
1970        assert!(selection.contains(&[2, 0, 0]));
1971        assert!(!selection.contains(&[1, 0, 0]));
1972    }
1973
1974    #[test]
1975    fn test_contains_union() {
1976        let selection = union(range(1..2, true_()), range(3..4, true_()));
1977        assert!(selection.contains(&[1, 0, 0]));
1978        assert!(!selection.contains(&[2, 0, 0]));
1979    }
1980
1981    #[test]
1982    #[should_panic(expected = "not implemented")]
1983    fn test_contains_any() {
1984        let selection = any(true_());
1985        selection.contains(&[0, 0, 0]);
1986    }
1987
1988    #[test]
1989    #[should_panic(expected = "not implemented")]
1990    fn test_contains_label() {
1991        let selection = label(vec!["zone".to_string()], true_());
1992        selection.contains(&[1, 2, 3]);
1993    }
1994
1995    #[test]
1996    #[should_panic(expected = "not implemented")]
1997    fn test_contains_first() {
1998        let selection = first(true_());
1999        selection.contains(&[0, 0, 0]);
2000    }
2001
2002    #[test]
2003    fn test_difference_1d() {
2004        assert_eq!(
2005            true_()
2006                .difference(
2007                    &EvalOpts::strict(),
2008                    &Slice::new_row_major([5]),
2009                    &[2usize, 4].into(),
2010                )
2011                .unwrap()
2012                .collect::<Vec<_>>(),
2013            vec![0, 1, 3]
2014        );
2015    }
2016
2017    #[test]
2018    fn test_difference_empty_selection() {
2019        assert_eq!(
2020            false_()
2021                .difference(
2022                    &EvalOpts::strict(),
2023                    &Slice::new_row_major([3]),
2024                    &[0usize, 1].into(),
2025                )
2026                .unwrap()
2027                .collect::<Vec<_>>(),
2028            Vec::<usize>::new()
2029        );
2030    }
2031
2032    #[test]
2033    fn test_difference_2d() {
2034        // [[0, 1, 2],
2035        //  [3, 4, 5]]
2036        // Select everything, exclude the second row.
2037        assert_eq!(
2038            all(all(true_()))
2039                .difference(
2040                    &EvalOpts::strict(),
2041                    &Slice::new_row_major([2, 3]),
2042                    &[3usize, 4, 5].into(),
2043                )
2044                .unwrap()
2045                .collect::<Vec<_>>(),
2046            vec![0, 1, 2]
2047        );
2048    }
2049
2050    #[test]
2051    fn test_of_ranks_1d() {
2052        let slice = Slice::new_row_major([5]);
2053        let ranks = BTreeSet::from([1, 3]);
2054        let selection = Selection::of_ranks(&slice, &ranks).unwrap();
2055        assert_eq!(
2056            selection
2057                .eval(&EvalOpts::strict(), &slice)
2058                .unwrap()
2059                .collect::<Vec<_>>(),
2060            vec![1, 3]
2061        )
2062    }
2063
2064    #[test]
2065    fn test_of_ranks_empty_set() {
2066        let slice = Slice::new_row_major([4]);
2067        let ranks = BTreeSet::new();
2068        let selection = Selection::of_ranks(&slice, &ranks).unwrap();
2069        assert_eq!(
2070            selection
2071                .eval(&EvalOpts::strict(), &slice)
2072                .unwrap()
2073                .collect::<Vec<_>>(),
2074            Vec::<usize>::new()
2075        )
2076    }
2077
2078    #[test]
2079    fn test_of_ranks_singleton_structural() {
2080        let slice = Slice::new_row_major([5]);
2081        let ranks = BTreeSet::from([2]);
2082        let actual = Selection::of_ranks(&slice, &ranks).unwrap();
2083        let expected = range(2..=2, true_());
2084        assert_structurally_eq!(&actual, &expected);
2085    }
2086
2087    #[test]
2088    fn test_of_ranks_union_2d_structural() {
2089        let slice = Slice::new_row_major([2, 3]);
2090        // [ [0, 1, 2],
2091        //   [3, 4, 5] ]
2092        // We'll select (0, 2), (1, 0) and (1, 1).
2093        let ranks = BTreeSet::from([2, 3, 4]);
2094        let actual = Selection::of_ranks(&slice, &ranks).unwrap();
2095        // Each rank becomes a nested selection:
2096        // 2 -> (0, 2) -> range(0, range(2, true_()))
2097        // 3 -> (1, 0) -> range(1, range(0, true_()))
2098        // 4 -> (1, 1) -> range(1, range(1, true_()))
2099        //
2100        // Their union is:
2101        let expected = union(
2102            union(range(0, range(2, true_())), range(1, range(0, true_()))),
2103            range(1, range(1, true_())),
2104        );
2105        assert_structurally_eq!(&actual, &expected);
2106    }
2107
2108    #[test]
2109    fn test_of_ranks_3d_structural() {
2110        let slice = Slice::new_row_major([2, 2, 2]);
2111        // [ [ [0, 1],
2112        //     [2, 3] ],
2113        //   [ [4, 5],
2114        //     [6, 7] ] ]
2115        let ranks = BTreeSet::from([1, 6]);
2116        let actual = Selection::of_ranks(&slice, &ranks).unwrap();
2117        let expected = union(
2118            range(0, range(0, range(1, true_()))), // (0, 0, 1)
2119            range(1, range(1, range(0, true_()))), // (1, 1, 0)
2120        );
2121        assert_structurally_eq!(&actual, &expected);
2122    }
2123
2124    #[test]
2125    fn test_of_ranks_invalid_index() {
2126        let slice = Slice::new_row_major([4]);
2127        let ranks = BTreeSet::from([0, 4]); // 4 is out of bounds
2128        assert!(
2129            Selection::of_ranks(&slice, &ranks).is_err(),
2130            "expected out-of-bounds error"
2131        );
2132    }
2133
2134    #[test]
2135    fn test_reify_slice_empty() {
2136        let slice = Slice::new_row_major([0]);
2137        let selection = slice.reify_slice(&slice).unwrap();
2138        let expected = false_();
2139        assert_structurally_eq!(&selection, expected);
2140        assert_eq!(
2141            selection
2142                .eval(&EvalOpts::lenient(), &slice)
2143                .unwrap()
2144                .collect::<Vec<_>>(),
2145            Vec::<usize>::new()
2146        );
2147    }
2148
2149    #[test]
2150    fn test_reify_slice_1d() {
2151        let shape = shape!(x = 6); // 1D shape with 6 elements
2152        let base = shape.slice();
2153
2154        let selected = select!(shape, x = 2..5).unwrap();
2155        let view = selected.slice();
2156
2157        let selection = base.reify_slice(view).unwrap();
2158        let expected = range(2..5, true_());
2159        assert_structurally_eq!(&selection, expected);
2160
2161        let flat: Vec<_> = selection.eval(&EvalOpts::strict(), base).unwrap().collect();
2162        assert_eq!(flat, vec![2, 3, 4]);
2163    }
2164
2165    #[test]
2166    fn test_reify_slice_2d() {
2167        let shape = shape!(x = 4, y = 5); // 2D shape: 4 rows, 5 columns
2168        let base = shape.slice();
2169
2170        // Select the middle 2x3 block: rows 1..3 and columns 2..5
2171        let selected = select!(shape, x = 1..3, y = 2..5).unwrap();
2172        let view = selected.slice();
2173        let selection = base.reify_slice(view).unwrap();
2174        let expected = range(1..3, range(2..5, true_()));
2175        assert_structurally_eq!(&selection, expected);
2176
2177        let flat: Vec<_> = selection.eval(&EvalOpts::strict(), base).unwrap().collect();
2178        assert_eq!(
2179            flat,
2180            vec![
2181                base.location(&[1, 2]).unwrap(),
2182                base.location(&[1, 3]).unwrap(),
2183                base.location(&[1, 4]).unwrap(),
2184                base.location(&[2, 2]).unwrap(),
2185                base.location(&[2, 3]).unwrap(),
2186                base.location(&[2, 4]).unwrap(),
2187            ]
2188        );
2189    }
2190
2191    #[test]
2192    #[allow(clippy::identity_op)]
2193    fn test_reify_slice_1d_with_stride() {
2194        let shape = shape!(x = 7); // 1D shape with 7 elements
2195        let selected = shape.select("x", Range(0, None, 2)).unwrap();
2196        let view = selected.slice();
2197        assert_eq!(view, &Slice::new(0, vec![4], vec![1 * 2]).unwrap());
2198
2199        let base = shape.slice();
2200        let selection = base.reify_slice(view).unwrap();
2201        // Note: ceil(7 / 2) = 4, hence end = 0 + 2 × 4 = 8. See the
2202        // more detailed explanation in
2203        // `test_reify_slice_2d_with_stride`.
2204        let expected = range(Range(0, Some(8), 2), true_());
2205        assert_structurally_eq!(&selection, expected);
2206
2207        let flat: Vec<_> = selection.eval(&EvalOpts::strict(), base).unwrap().collect();
2208        assert_eq!(flat, vec![0, 2, 4, 6]);
2209    }
2210
2211    #[test]
2212    #[allow(clippy::identity_op)]
2213    fn test_reify_slice_2d_with_stride() {
2214        // 4 x 4: x = 4, y = 4.
2215        let base = shape!(x = 4, y = 4);
2216        // Step 1: select odd rows (x = 1..4 step 2)
2217        let shape = base.select("x", Range(1, Some(4), 2)).unwrap();
2218        // Step 2: then select odd columns (y = 1..4 step 2)
2219        let shape = shape.select("y", Range(1, Some(4), 2)).unwrap();
2220        let view = shape.slice();
2221        assert_eq!(
2222            view,
2223            &Slice::new(5, vec![2, 2], vec![4 * 2, 1 * 2]).unwrap()
2224        );
2225
2226        let base = base.slice();
2227        let selection = base.reify_slice(view).unwrap();
2228        // We use `end = start + step * len` to reify the selection.
2229        // Note: This may yield `end > original_end` (e.g., 5 instead of 4)
2230        // when the selection length was computed via ceiling division.
2231        // This is safe: the resulting range will still select the correct
2232        // indices (e.g., 1 and 3 for Range(1, Some(5), 2)).
2233        let expected = range(Range(1, Some(5), 2), range(Range(1, Some(5), 2), true_()));
2234        assert_structurally_eq!(&selection, expected);
2235
2236        let flat: Vec<_> = selection.eval(&EvalOpts::strict(), base).unwrap().collect();
2237        assert_eq!(flat, vec![5, 7, 13, 15]);
2238    }
2239
2240    #[test]
2241    fn test_reify_slice_selects_column_across_rows() {
2242        let shape = shape!(host = 2, gpu = 4); // shape [2, 4]
2243        let base = shape.slice();
2244
2245        // Select the 3rd GPU (index 2) across both hosts
2246        let selected = select!(shape, gpu = 2).unwrap(); // (0, 2) and (1, 2)
2247        let view = selected.slice();
2248        let coordinates: Vec<_> = view.iter().map(|i| view.coordinates(i).unwrap()).collect();
2249        assert_eq!(coordinates, [[0, 0], [1, 0]]);
2250
2251        let selection = base.reify_slice(view).unwrap();
2252        let expected = range(0..2, range(2..3, true_()));
2253        assert_structurally_eq!(&selection, expected);
2254
2255        let actual = selection
2256            .eval(&EvalOpts::strict(), base)
2257            .unwrap()
2258            .collect::<Vec<_>>();
2259        assert_eq!(
2260            actual,
2261            vec![
2262                base.location(&[0, 2]).unwrap(),
2263                base.location(&[1, 2]).unwrap()
2264            ]
2265        );
2266    }
2267
2268    #[test]
2269    fn test_reify_slice_dimension_mismatch() {
2270        let shape = shape!(host = 2, gpu = 4);
2271        let base = shape.slice();
2272
2273        // Select the 3rd GPU (index 2) across both hosts i.e. flat
2274        // indices [2, 6]
2275        let indices = vec![
2276            base.location(&[0, 2]).unwrap(),
2277            base.location(&[1, 2]).unwrap(),
2278        ];
2279
2280        let view = Slice::new(indices[0], vec![indices.len()], vec![4]).unwrap();
2281        let selection = base.reify_slice(&view).unwrap();
2282
2283        let expected = Selection::of_ranks(base, &indices.iter().cloned().collect()).unwrap();
2284        assert_structurally_eq!(&selection, expected);
2285
2286        let actual: Vec<_> = selection.eval(&EvalOpts::strict(), base).unwrap().collect();
2287        assert_eq!(actual, indices);
2288    }
2289
2290    #[test]
2291    fn test_union_of_slices_empty() {
2292        let base = Slice::new_row_major([2]);
2293        let sel = base.reify_slices(&[]).unwrap();
2294        assert_structurally_eq!(&sel, &false_());
2295        assert_eq!(
2296            sel.eval(&EvalOpts::strict(), &base)
2297                .unwrap()
2298                .collect::<Vec<_>>(),
2299            Vec::<usize>::new()
2300        );
2301    }
2302
2303    #[test]
2304    fn test_union_of_slices_singleton() {
2305        let shape = shape!(x = 3);
2306        let base = shape.slice();
2307        let selected = select!(shape, x = 1).unwrap();
2308        let view = selected.slice().clone();
2309
2310        let selection = base.reify_slices(&[view]).unwrap();
2311        let expected = range(1..=1, true_());
2312        assert_structurally_eq!(&selection, &expected);
2313
2314        assert_eq!(
2315            selection
2316                .eval(&EvalOpts::strict(), base)
2317                .unwrap()
2318                .collect::<Vec<_>>(),
2319            vec![1],
2320        );
2321    }
2322
2323    #[test]
2324    fn test_union_of_slices_disjoint() {
2325        let shape = shape!(x = 2, y = 2); // 2x2 grid
2326        let base = shape.slice();
2327
2328        // View A: (0, *)
2329        let a = select!(shape, x = 0).unwrap();
2330        let view_a = a.slice().clone();
2331
2332        // View B: (1, *)
2333        let b = select!(shape, x = 1).unwrap();
2334        let view_b = b.slice().clone();
2335
2336        let selection = base.reify_slices(&[view_a, view_b]).unwrap();
2337        let expected = union(
2338            range(0..1, range(0..2, true_())),
2339            range(1..2, range(0..2, true_())),
2340        );
2341        assert_structurally_eq!(&selection, &expected);
2342        assert_eq!(
2343            selection
2344                .eval(&EvalOpts::strict(), base)
2345                .unwrap()
2346                .collect::<Vec<_>>(),
2347            base.iter().collect::<Vec<_>>()
2348        );
2349    }
2350
2351    #[test]
2352    fn test_union_of_slices_overlapping() {
2353        let shape = shape!(x = 1, y = 4); // 1x4 grid
2354        let base = shape.slice();
2355
2356        let selected1 = select!(shape, y = 0..2).unwrap();
2357        let view1 = selected1.slice().clone();
2358
2359        let selected2 = select!(shape, y = 1..4).unwrap();
2360        let view2 = selected2.slice().clone();
2361
2362        let selection = base.reify_slices(&[view1, view2]).unwrap();
2363        let expected = union(
2364            range(0..1, range(0..2, true_())),
2365            range(0..1, range(1..4, true_())),
2366        );
2367        assert_structurally_eq!(&selection, &expected);
2368
2369        assert_eq!(
2370            selection
2371                .eval(&EvalOpts::strict(), base)
2372                .unwrap()
2373                .collect::<Vec<_>>(),
2374            base.iter().collect::<Vec<_>>()
2375        );
2376    }
2377}