ndslice/selection/
parse.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 parser of a compact syntax used to
10//! describe hierarchical selections over multidimensional meshes.
11//! ```text
12//! expression       ::= union
13//! union            ::= intersection ( "|" intersection )*
14//! intersection     ::= chain ( "&" chain )*
15//! chain            ::= group ( "," group )*
16//! group            ::= range
17//!                    | index
18//!                    | wildcard
19//!                    | any
20//!                    | "(" expression ")"
21//! range            ::= number? ":" number? ( ":" number )?
22//! index            ::= number
23//! wildcard         ::= "*"
24//! any              ::= "?"
25//! number           ::= [0-9]+
26//! ```
27//!
28//! Notes:
29//! - `,` separates **nested dimensions** (i.e., descent into next
30//!   dimension).
31//! - `|` is union, `&` is intersection. `&` binds tighter than `|`.
32//! - `*` selects all values at the current dimension and descends.
33//! - `?` selects a random value at the current dimension and descends.
34//! - A range like `2:5:1` has the form `start:end:step`. Missing
35//!   parts default to:
36//!     - `start = 0`
37//!     - `end = full extent`
38//!     - `step = 1`
39//! - An index like `3` is shorthand for the range `3:4`.
40//! - Parentheses `()` allow grouping for precedence control and
41//!   nesting of chains.
42//! - Whitespace is not allowed (although the `parse` function will
43//!   admit and strip it.)
44//! - Expressions like `(*,*,1:4|5:6)` are valid and parsed as:
45//!     - A union of two expressions:
46//!         1. The chain `*,*,1:4`
47//!         2. The standalone `5:6`
48//!     - The union applies at the top level, not just within a dimension.
49//!     - To apply the union **only to the third dimension**, parentheses must be used:
50//!       e.g., `*,*,(1:4|5:6)`
51
52use nom::IResult;
53use nom::Parser as _;
54use nom::branch::alt;
55use nom::bytes::complete::tag;
56use nom::character::complete::char;
57use nom::character::complete::digit1;
58use nom::combinator::map;
59use nom::combinator::map_res;
60use nom::combinator::opt;
61use nom::multi::separated_list1;
62use nom::sequence::delimited;
63use nom::sequence::preceded;
64
65use crate::selection::Selection;
66use crate::selection::dsl;
67use crate::shape;
68
69fn number(input: &str) -> IResult<&str, usize> {
70    map_res(digit1, str::parse).parse(input)
71}
72
73fn index(input: &str) -> IResult<&str, Selection> {
74    map(number, |n| dsl::range(n, dsl::true_())).parse(input)
75}
76
77fn range(input: &str) -> IResult<&str, Selection> {
78    let (input, (start, end, step)) = (
79        opt(number),
80        preceded(char(':'), opt(number)),
81        opt(preceded(char(':'), number)),
82    )
83        .parse(input)?;
84
85    Ok((
86        input,
87        dsl::range(
88            shape::Range(start.unwrap_or(0), end, step.unwrap_or(1)),
89            dsl::true_(),
90        ),
91    ))
92}
93
94fn wildcard(input: &str) -> IResult<&str, Selection> {
95    map(tag("*"), |_| dsl::all(dsl::true_())).parse(input)
96}
97
98fn any(input: &str) -> IResult<&str, Selection> {
99    map(tag("?"), |_| dsl::any(dsl::true_())).parse(input)
100}
101
102fn group(input: &str) -> IResult<&str, Selection> {
103    alt((
104        delimited(char('('), expression, char(')')),
105        range,
106        index,
107        wildcard,
108        any,
109    ))
110    .parse(input)
111}
112
113fn chain(input: &str) -> IResult<&str, Selection> {
114    map(separated_list1(char(','), group), |dims| {
115        dims.into_iter()
116            .rev()
117            .fold(dsl::true_(), |acc, dim| nest(dim, acc))
118    })
119    .parse(input)
120}
121
122// Recursively nests `dim` into `tail`, modeling `,` (dimension
123// descent).
124//
125// For example, the chain `*,1:4,?` is parsed as:
126//   [All(True), Range(1..4, True), Any(True)]
127//
128// Nesting proceeds right to left:
129//   Any(True) → Range(1..4, Any(True)) → All(Range(1..4, Any(True)))
130//
131// Unions and intersections nest both branches.
132fn nest(dim: Selection, tail: Selection) -> Selection {
133    match dim {
134        Selection::All(inner) => dsl::all(nest(*inner, tail)),
135        Selection::Any(inner) => dsl::any(nest(*inner, tail)),
136        Selection::Range(r, inner) => dsl::range(r, nest(*inner, tail)),
137        Selection::Union(a, b) => dsl::union(nest(*a, tail.clone()), nest(*b, tail)),
138        Selection::Intersection(a, b) => dsl::intersection(nest(*a, tail.clone()), nest(*b, tail)),
139        Selection::True => tail,
140        Selection::False => dsl::false_(),
141        other => panic!("unexpected selection variant in chain: {:?}", other),
142    }
143}
144
145fn intersection(input: &str) -> IResult<&str, Selection> {
146    map(separated_list1(char('&'), chain), |items| {
147        items.into_iter().reduce(dsl::intersection).unwrap()
148    })
149    .parse(input)
150}
151
152pub fn expression(input: &str) -> IResult<&str, Selection> {
153    map(separated_list1(char('|'), intersection), |items| {
154        items.into_iter().reduce(dsl::union).unwrap()
155    })
156    .parse(input)
157}
158
159/// Parses a selection expression from a string, ignoring all
160/// whitespace.
161///
162/// # Arguments
163///
164/// * `input` - A string slice containing the selection expression to
165///   parse.
166///
167/// # Returns
168///
169/// * `Ok(Selection)` if parsing succeeds
170/// * `Err(anyhow::Error)` with a detailed error message if parsing
171///   fails
172pub fn parse(input: &str) -> anyhow::Result<Selection> {
173    use nom::combinator::all_consuming;
174
175    let input: String = input.chars().filter(|c| !c.is_whitespace()).collect();
176    let (_, selection) = all_consuming(expression)
177        .parse(&input)
178        .map_err(|err| anyhow::anyhow!("Failed to parse selection: {err:?} (input: {input:?})"))?;
179    Ok(selection)
180}
181
182#[cfg(test)]
183mod tests {
184    use crate::selection::Selection;
185    use crate::shape;
186
187    // Parse an input string to a selection.
188    fn parse(input: &str) -> Selection {
189        super::parse(input).unwrap()
190    }
191
192    #[macro_export]
193    macro_rules! assert_parses_to {
194        ($input:expr_2021, $expected:expr_2021) => {{
195            let actual = $crate::selection::parse::tests::parse($input);
196            $crate::assert_structurally_eq!($expected, actual);
197        }};
198    }
199
200    #[test]
201    fn test_selection_11() {
202        use crate::selection::dsl::*;
203
204        assert_parses_to!("*", all(true_()));
205        assert_parses_to!("*,*", all(all(true_())));
206        assert_parses_to!("*,*,*", all(all(all(true_()))));
207
208        assert_parses_to!("4:8", range(shape::Range(4, Some(8), 1), true_()));
209        assert_parses_to!("4:", range(shape::Range(4, None, 1), true_()));
210        assert_parses_to!("4", range(shape::Range(4, Some(5), 1), true_()));
211        assert_parses_to!(":", range(shape::Range(0, None, 1), true_()));
212
213        assert_parses_to!("0,0,0", range(0, range(0, range(0, true_()))));
214        assert_parses_to!("1,1,1", range(1, range(1, range(1, true_()))));
215        assert_parses_to!("*,0", all(range(0, true_())));
216        assert_parses_to!("*,0,*", all(range(0, all(true_()))));
217        assert_parses_to!("*,0,*", all(range(0, all(true_()))));
218        assert_parses_to!("*,*,4:", all(all(range(4.., true_()))));
219        assert_parses_to!(
220            "*,*,1::2",
221            all(all(range(shape::Range(1, None, 2), true_())))
222        );
223
224        assert_parses_to!(
225            "*,*,:4|*,*,4:",
226            union(
227                all(all(range(0..4, true_()))),
228                all(all(range(shape::Range(4, None, 1), true_())))
229            )
230        );
231        assert_parses_to!(
232            "*,0,:4|*,1,4:8",
233            union(
234                all(range(0, range(0..4, true_()))),
235                all(range(1, range(4..8, true_()))),
236            )
237        );
238        assert_parses_to!(
239            "*,*,(:2|6:)",
240            all(all(union(
241                range(0..2, true_()),
242                range(shape::Range(6, None, 1), true_()),
243            )))
244        );
245        assert_parses_to!(
246            "*,*,(1:4:2|5::2)",
247            all(all(union(
248                range(shape::Range(1, Some(4), 2), true_()),
249                range(shape::Range(5, None, 2), true_()),
250            )))
251        );
252        assert_parses_to!("*&*", intersection(all(true_()), all(true_())));
253        assert_parses_to!(
254            "*&*,*,4:8",
255            intersection(all(true_()), all(all(range(4..8, true_()))))
256        );
257        assert_parses_to!(
258            "*,*,0:5&*,*,4:8",
259            intersection(
260                all(all(range(0..5, true_()))),
261                all(all(range(4..8, true_()))),
262            )
263        );
264
265        assert_parses_to!("?,?,?", any(any(any(true_()))));
266        assert_parses_to!("0,?,:4", range(0, any(range(0..4, true_()))));
267        assert_parses_to!("0,?", range(0, any(true_())));
268        assert_parses_to!("?", any(true_()));
269        assert_parses_to!(
270            "0,0,?|0,0,?",
271            union(
272                range(0, range(0, any(true_()))),
273                range(0, range(0, any(true_()))),
274            )
275        );
276        assert_parses_to!(
277            "*,*,1:4|5:6",
278            union(all(all(range(1..4, true_()))), range(5..6, true_()))
279        );
280        assert_parses_to!(
281            "*,*,(1:4|5:6)",
282            all(all(union(range(1..4, true_()), range(5..6, true_()))))
283        );
284        assert_parses_to!(
285            "*,(1:4|5:6),*",
286            all(union(
287                range(shape::Range(1, Some(4), 1), all(true_())),
288                range(shape::Range(5, Some(6), 1), all(true_())),
289            ))
290        );
291        assert_parses_to!(
292            "(*,*,*)&(*,*,*)",
293            intersection(all(all(all(true_()))), all(all(all(true_()))))
294        );
295        assert_parses_to!(
296            "(0,*,*)&(0,(1|3),*)",
297            intersection(
298                range(0, all(all(true_()))),
299                range(0, union(range(1, all(true_())), range(3, all(true_())))),
300            )
301        );
302        assert_parses_to!(
303            "(*,*,(:2|6:))&(*,*,4:)",
304            intersection(
305                all(all(union(
306                    range(0..2, true_()),
307                    range(shape::Range(6, None, 1), true_()),
308                ))),
309                all(all(range(shape::Range(4, None, 1), true_()))),
310            )
311        );
312        assert_parses_to!("((1:4),2)", range(1..4, range(2, true_())));
313    }
314
315    #[test]
316    fn test_12() {
317        use crate::dsl::all;
318        use crate::dsl::true_;
319
320        assert_parses_to!("*,*,*,*,*,*", all(all(all(all(all(all(true_())))))));
321    }
322}