ndslice/selection/
pretty.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//! Pretty-printing utilities for selection expressions.
10//!
11//! This module defines `SelectionSYM` implementations that render
12//! selection expressions in human-readable or structured forms.
13//!
14//! The `Display` implementation for [`Selection`] delegates to this
15//! module and uses the `SelectionPretty` representation.
16use crate::Selection;
17use crate::selection::LabelKey;
18use crate::selection::SelectionSYM;
19use crate::shape;
20
21/// A structured pretty-printer that renders [`Selection`] expressions
22/// in DSL constructor form.
23///
24/// This type implements [`SelectionSYM`] and emits expressions like
25/// `all(range(0..4, true_()))`, which mirror the Rust-based DSL used
26/// to construct `Selection` values programmatically.
27///
28/// Internally used by [`Selection::fmt`] to support human-readable
29/// display of selection expressions in their canonical constructor
30/// form.
31///
32/// Use [`Selection::fold`] or the [`pretty`] helper to produce a
33/// `SelectionPretty`:
34///
35/// ```rust
36/// use ndslice::selection::dsl::*;
37/// use ndslice::selection::pretty::pretty;
38/// let expr = all(range(0..4, true_()));
39/// println!("{}", pretty(&expr)); // prints: all(range(0..4, true_()))
40/// ```
41pub struct SelectionPretty(String);
42
43impl std::fmt::Display for SelectionPretty {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        f.write_str(&self.0)
46    }
47}
48
49impl SelectionSYM for SelectionPretty {
50    fn false_() -> Self {
51        SelectionPretty("false_()".into())
52    }
53    fn true_() -> Self {
54        SelectionPretty("true_()".into())
55    }
56    fn all(s: Self) -> Self {
57        SelectionPretty(format!("all({})", s.0))
58    }
59    fn first(s: Self) -> Self {
60        SelectionPretty(format!("first({})", s.0))
61    }
62    fn range<R: Into<shape::Range>>(range: R, s: Self) -> Self {
63        let r = range.into();
64        SelectionPretty(format!("range({}, {})", r, s.0))
65    }
66    fn label<L: Into<LabelKey>>(labels: Vec<L>, s: Self) -> Self {
67        let labels_str = labels
68            .into_iter()
69            .map(|l| l.into().to_string())
70            .collect::<Vec<_>>()
71            .join(", ");
72        SelectionPretty(format!("label([{}], {})", labels_str, s.0))
73    }
74    fn any(s: Self) -> Self {
75        SelectionPretty(format!("any({})", s.0))
76    }
77    fn intersection(a: Self, b: Self) -> Self {
78        SelectionPretty(format!("intersection({}, {})", a.0, b.0))
79    }
80    fn union(a: Self, b: Self) -> Self {
81        SelectionPretty(format!("union({}, {})", a.0, b.0))
82    }
83}
84
85/// Renders a [`Selection`] as a structured DSL expression.
86///
87/// This function folds the input selection using the [`SelectionSYM`]
88/// interface, producing a [`SelectionPretty`] — a human-readable
89/// representation in canonical constructor form (e.g.,
90/// `all(range(0..4, true_()))`).
91///
92/// Useful for debugging, diagnostics, and implementing `Display`.
93///
94/// # Example
95///
96/// ```rust
97/// use ndslice::selection::dsl::*;
98/// use ndslice::selection::pretty::pretty;
99/// let expr = all(range(0..4, true_()));
100/// println!("{}", pretty(&expr)); // prints: all(range(0..4, true_()))
101/// ```
102pub fn pretty(selection: &Selection) -> SelectionPretty {
103    selection.fold()
104}
105
106/// A structured formatter that renders [`Selection`] expressions in
107/// compact surface syntax.
108///
109/// This type implements [`SelectionSYM`] and emits selection
110/// expressions using the same textual format accepted by both:
111///
112/// - The [`sel!`] macro (defined in the `hypermesh_macros` crate)
113/// - The string-based [`parse`](crate::selection::parse::parse)
114///   function
115///
116/// Examples of this syntax include:
117///
118/// - `*`
119/// - `0, 1..4, *`
120/// - `["A100"]?`
121/// - `(0, (0 | 2), *) & (0, *, *)`
122///   — intersection of two 3D expressions; simplifies to just `0, (0
123///     | 2), *` since the second operand is a superset
124///
125/// Used internally by the [`compact`] helper and [`Selection::fmt`]
126/// to produce concise, user-facing representations of selection
127/// expressions.
128///
129/// # Example
130///
131/// ```rust
132/// use ndslice::selection::dsl::*;
133/// use ndslice::selection::pretty::compact;
134/// let expr = all(range(0..4, true_()));
135/// println!("{}", compact(&expr)); // prints: (*, 0..4)
136/// ```
137pub struct SelectionCompact(String);
138
139impl std::fmt::Display for SelectionCompact {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        f.write_str(&self.0)
142    }
143}
144
145impl SelectionSYM for SelectionCompact {
146    fn true_() -> Self {
147        SelectionCompact("".into())
148    }
149
150    fn false_() -> Self {
151        panic!("SelectionCompact: `false` has no compact surface syntax")
152    }
153
154    fn first(_: Self) -> Self {
155        panic!("SelectionCompact: `first` has no compact surface syntax")
156    }
157
158    fn all(s: Self) -> Self {
159        if s.0.is_empty() {
160            SelectionCompact("*".into())
161        } else {
162            SelectionCompact(format!("*,{}", s.0))
163        }
164    }
165
166    fn range<R: Into<shape::Range>>(range: R, s: Self) -> Self {
167        let r = range.into();
168        let range_str = match (r.0, r.1, r.2) {
169            (start, Some(end), 1) => format!("{}:{}", start, end),
170            (start, Some(end), step) => format!("{}:{}:{}", start, end, step),
171            (start, None, step) => format!("{}::{}", start, step),
172        };
173        if s.0.is_empty() {
174            SelectionCompact(range_str)
175        } else {
176            SelectionCompact(format!("{},{}", range_str, s.0))
177        }
178    }
179
180    fn label<L: Into<LabelKey>>(labels: Vec<L>, s: Self) -> Self {
181        let label_str = labels
182            .into_iter()
183            .map(|l| l.into().to_string())
184            .collect::<Vec<_>>()
185            .join(",");
186        if s.0.is_empty() {
187            panic!("SelectionCompact: label requires a combinator like '*', '?', or a range");
188        }
189        SelectionCompact(format!("[{}]{}", label_str, s.0))
190    }
191
192    fn any(s: Self) -> Self {
193        if s.0.is_empty() {
194            SelectionCompact("?".into())
195        } else {
196            SelectionCompact(format!("?,{}", s.0))
197        }
198    }
199
200    fn intersection(a: Self, b: Self) -> Self {
201        SelectionCompact(format!("({}&{})", a.0, b.0))
202    }
203
204    fn union(a: Self, b: Self) -> Self {
205        SelectionCompact(format!("({}|{})", a.0, b.0))
206    }
207}
208
209/// Returns a [`SelectionCompact`] rendering of the given
210/// [`Selection`] expression.
211///
212/// This produces a string in the surface syntax used by the `sel!`
213/// macro and the [`parse`] function, such as:
214///
215/// ```
216/// use ndslice::selection::Selection;
217/// use ndslice::selection::dsl::*;
218/// use ndslice::selection::pretty::compact;
219///
220/// let sel = all(range(0..4, true_()));
221/// assert_eq!(compact(&sel).to_string(), "*,0:4");
222/// ```
223/// [`parse`]: crate::selection::parse::parse
224pub fn compact(selection: &Selection) -> SelectionCompact {
225    selection.fold()
226}
227
228#[cfg(test)]
229mod tests {
230    use crate::assert_round_trip;
231    use crate::selection::Selection;
232    use crate::shape;
233
234    #[test]
235    fn test_selection_to_compact_and_back() {
236        use crate::selection::dsl::*;
237
238        assert_round_trip!(all(true_()));
239        assert_round_trip!(all(all(true_())));
240        assert_round_trip!(all(all(all(true_()))));
241
242        assert_round_trip!(range(shape::Range(4, Some(8), 1), true_()));
243        assert_round_trip!(range(shape::Range(4, None, 1), true_()));
244        assert_round_trip!(range(shape::Range(4, Some(5), 1), true_()));
245        assert_round_trip!(range(shape::Range(0, None, 1), true_()));
246
247        assert_round_trip!(range(0, range(0, range(0, true_()))));
248        assert_round_trip!(range(1, range(1, range(1, true_()))));
249        assert_round_trip!(all(range(0, true_())));
250        assert_round_trip!(all(range(0, all(true_()))));
251
252        assert_round_trip!(all(all(range(4.., true_()))));
253        assert_round_trip!(all(all(range(shape::Range(1, None, 2), true_()))));
254
255        assert_round_trip!(union(
256            all(all(range(0..4, true_()))),
257            all(all(range(shape::Range(4, None, 1), true_()))),
258        ));
259        assert_round_trip!(union(
260            all(range(0, range(0..4, true_()))),
261            all(range(1, range(4..8, true_()))),
262        ));
263        assert_round_trip!(union(
264            all(all(range(0..2, true_()))),
265            all(all(range(shape::Range(6, None, 1), true_()))),
266        ));
267        assert_round_trip!(union(
268            all(all(range(shape::Range(1, Some(4), 2), true_()))),
269            all(all(range(shape::Range(5, None, 2), true_()))),
270        ));
271        assert_round_trip!(intersection(all(true_()), all(true_())));
272        assert_round_trip!(intersection(all(true_()), all(all(range(4..8, true_())))));
273        assert_round_trip!(intersection(
274            all(all(range(0..5, true_()))),
275            all(all(range(4..8, true_()))),
276        ));
277
278        assert_round_trip!(any(any(any(true_()))));
279        assert_round_trip!(range(0, any(range(0..4, true_()))));
280        assert_round_trip!(range(0, any(true_())));
281        assert_round_trip!(any(true_()));
282        assert_round_trip!(union(
283            range(0, range(0, any(true_()))),
284            range(0, range(0, any(true_()))),
285        ));
286        assert_round_trip!(union(all(all(range(1..4, true_()))), range(5..6, true_())));
287        assert_round_trip!(all(all(union(range(1..4, true_()), range(5..6, true_())))));
288        assert_round_trip!(all(union(
289            range(shape::Range(1, Some(4), 1), all(true_())),
290            range(shape::Range(5, Some(6), 1), all(true_())),
291        )));
292        assert_round_trip!(intersection(all(all(all(true_()))), all(all(all(true_()))),));
293        assert_round_trip!(intersection(
294            range(0, all(all(true_()))),
295            range(0, union(range(1, all(true_())), range(3, all(true_())))),
296        ));
297        assert_round_trip!(intersection(
298            all(all(union(
299                range(0..2, true_()),
300                range(shape::Range(6, None, 1), true_()),
301            ))),
302            all(all(range(shape::Range(4, None, 1), true_()))),
303        ));
304        assert_round_trip!(range(1..4, range(2, true_())));
305
306        // TODO(SF, 2025-05-05): There isn't parse support for `label`
307        // yet.
308        // assert_round_trip!(label(vec!["A100"], all(true_())));
309    }
310}