flow_expression_parser/parse/
v1.rs

1mod parsers;
2
3use nom::branch::alt;
4use nom::bytes::complete::tag;
5use nom::character::complete::{alpha1, alphanumeric1, char, digit1, multispace0};
6use nom::combinator::{eof, map, opt, recognize};
7use nom::error::ParseError;
8use nom::multi::{many0, many1};
9use nom::sequence::{delimited, pair, preceded, terminated};
10use nom::IResult;
11
12use crate::ast::{
13  BlockExpression,
14  ConnectionExpression,
15  ConnectionTargetExpression,
16  FlowExpression,
17  FlowProgram,
18  InstancePort,
19  InstanceTarget,
20};
21use crate::Error;
22
23/// The separator in a connection between connection targets.
24pub static CONNECTION_SEPARATOR: &str = "->";
25
26/// The reserved identifier representing an as-of-yet-undetermined default value.
27const DEFAULT_ID: &str = "<>";
28
29#[derive(Debug, Clone, Copy)]
30#[non_exhaustive]
31/// Errors that can occur during parsing.
32pub enum ParserError {
33  /// General parse failure.
34  Fail,
35  /// Unexpected token.
36  UnexpectedToken,
37}
38
39impl std::error::Error for ParserError {}
40impl std::fmt::Display for ParserError {
41  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42    match self {
43      ParserError::Fail => write!(f, "Parse failed"),
44      ParserError::UnexpectedToken => write!(f, "Unexpected token"),
45    }
46  }
47}
48
49/// Parse a string into a [FlowProgram].
50pub fn parse(input: &str) -> Result<FlowProgram, ParserError> {
51  _parse(input).map(|(_, t)| t).map_err(|_| ParserError::Fail)
52}
53
54fn component_id(input: &str) -> IResult<&str, InstanceTarget> {
55  let (i, t) = recognize(alt((reserved_component_id, identifier)))(input)?;
56  let t = match t {
57    super::SCHEMATIC_INPUT => InstanceTarget::Input,
58    super::SCHEMATIC_OUTPUT => InstanceTarget::Output,
59    super::SCHEMATIC_NULL | "drop" => InstanceTarget::Null(None),
60    super::CORE_ID => InstanceTarget::Core,
61    DEFAULT_ID => InstanceTarget::Default,
62    super::NS_LINK => InstanceTarget::Link,
63    name => InstanceTarget::Named(name.to_owned()),
64  };
65  Ok((i, t))
66}
67
68pub(crate) fn identifier(input: &str) -> IResult<&str, &str> {
69  recognize(pair(alt((alpha1, tag("_"))), many0(alt((alphanumeric1, tag("_"))))))(input)
70}
71
72fn reserved_component_id(input: &str) -> IResult<&str, &str> {
73  alt((
74    tag(super::SCHEMATIC_INPUT),
75    tag(super::SCHEMATIC_OUTPUT),
76    tag(super::SCHEMATIC_NULL),
77    tag("drop"),
78    tag(super::CORE_ID),
79    tag(super::NS_LINK),
80    tag(DEFAULT_ID),
81  ))(input)
82}
83
84fn operation_path(input: &str) -> IResult<&str, &str> {
85  recognize(pair(many1(terminated(identifier, tag("::"))), identifier))(input)
86}
87
88fn inline_id(input: &str) -> IResult<&str, &str> {
89  delimited(char('['), identifier, char(']'))(input)
90}
91
92fn component_path(input: &str) -> IResult<&str, InstanceTarget> {
93  let (i, (path_parts, id)) = pair(operation_path, opt(inline_id))(input)?;
94  Ok((
95    i,
96    id.map_or_else(
97      || InstanceTarget::anonymous_path(path_parts),
98      |id| InstanceTarget::path(path_parts, id),
99    ),
100  ))
101}
102
103fn instance(input: &str) -> IResult<&str, InstanceTarget> {
104  alt((component_path, component_id))(input)
105}
106
107pub(crate) fn instance_port(input: &str) -> IResult<&str, InstancePort> {
108  let (i, (name, parts)) = pair(
109    identifier,
110    many0(preceded(
111      char('.'),
112      alt((
113        map(identifier, |r: &str| r.to_owned()),
114        map(digit1, |r: &str| r.to_owned()),
115        parsers::parse_string,
116      )),
117    )),
118  )(input)?;
119  if parts.is_empty() {
120    Ok((i, InstancePort::Named(name.to_owned())))
121  } else {
122    Ok((i, InstancePort::Path(name.to_owned(), parts.into_iter().collect())))
123  }
124}
125
126fn connection_target_expression(input: &str) -> IResult<&str, (InstanceTarget, InstancePort)> {
127  pair(terminated(instance, char('.')), instance_port)(input).map(|(i, v)| (i, (v.0, v.1)))
128}
129
130fn portless_target_expression(input: &str) -> IResult<&str, (InstanceTarget, InstancePort)> {
131  instance(input).map(|(i, v)| (i, (v, InstancePort::None)))
132}
133
134fn connection_expression_sequence(input: &str) -> IResult<&str, FlowExpression> {
135  let (i, (from, hops)) = pair(
136    alt((connection_target_expression, portless_target_expression)),
137    many1(preceded(
138      ws(tag(CONNECTION_SEPARATOR)),
139      ws(alt((connection_target_expression, portless_target_expression))),
140    )),
141  )(input)?;
142
143  let mut connections = Vec::new();
144
145  let mut last_hop = from;
146  last_hop.0.ensure_id();
147  for mut hop in hops {
148    hop.0.ensure_id();
149    connections.push(FlowExpression::ConnectionExpression(Box::new(connect(
150      last_hop,
151      hop.clone(),
152    ))));
153    last_hop = hop;
154  }
155
156  if connections.len() == 1 {
157    Ok((i, connections.remove(0)))
158  } else {
159    Ok((i, FlowExpression::BlockExpression(BlockExpression::new(connections))))
160  }
161}
162
163fn connect(from: (InstanceTarget, InstancePort), to: (InstanceTarget, InstancePort)) -> ConnectionExpression {
164  let (from, to) = match (from, to) {
165    // if we have a known-default upstream and a port on the downstream, use the downstream port's name
166    ((from, InstancePort::None), (to, to_port))
167      if matches!(from, InstanceTarget::Input | InstanceTarget::Default)
168        && matches!(to_port, InstancePort::Named(_) | InstancePort::Path(_, _)) =>
169    {
170      ((from, InstancePort::named(to_port.name().unwrap())), (to, to_port))
171    }
172    // if we have a known-default downstream and a port on the upstream, use the upstream port's name
173    ((from, from_port), (to, InstancePort::None))
174      if matches!(to, InstanceTarget::Output | InstanceTarget::Default)
175        && matches!(from_port, InstancePort::Named(_) | InstancePort::Path(_, _)) =>
176    {
177      let to_port = InstancePort::named(from_port.name().unwrap());
178      ((from, from_port), (to, to_port))
179    }
180    // otherwise, pass it along for the next processor to deal with.
181    x => x,
182  };
183  ConnectionExpression::new(
184    ConnectionTargetExpression::new(from.0, from.1),
185    ConnectionTargetExpression::new(to.0, to.1),
186  )
187}
188
189pub(crate) fn flow_expression(input: &str) -> IResult<&str, FlowExpression> {
190  let (i, expr) = terminated(connection_expression_sequence, alt((eof, ws(tag(";")))))(input)?;
191  Ok((i, expr))
192}
193
194fn _parse(input: &str) -> IResult<&str, FlowProgram> {
195  let (i, expressions) = many0(flow_expression)(input)?;
196
197  Ok((i, FlowProgram::new(expressions)))
198}
199
200fn ws<'a, F, O, E: ParseError<&'a str>>(inner: F) -> impl FnMut(&'a str) -> IResult<&'a str, O, E>
201where
202  F: FnMut(&'a str) -> IResult<&'a str, O, E>,
203{
204  delimited(multispace0, inner, multispace0)
205}
206
207/// Parse a string into an InstanceTarget
208pub(crate) fn parse_instance(s: &str) -> Result<InstanceTarget, Error> {
209  let (_, c) = instance(s).map_err(|_e| Error::ComponentIdError(s.to_owned()))?;
210  Ok(c)
211}
212
213#[cfg(test)]
214mod tests {
215
216  use anyhow::Result;
217  use pretty_assertions::assert_eq;
218  use rstest::rstest;
219
220  use super::{
221    ConnectionExpression as CE,
222    ConnectionTargetExpression as CTE,
223    FlowExpression as FE,
224    InstancePort as InstPort,
225    InstanceTarget as InstTgt,
226    *,
227  };
228  use crate::ast::{set_seed, BlockExpression};
229  // use crate::ast::BlockExpression;
230
231  #[rstest]
232  #[case("<>", InstTgt::Default)]
233  #[case("<input>", InstTgt::Input)]
234  #[case("<output>", InstTgt::Output)]
235  #[case("core", InstTgt::Core)]
236  #[case("heya", InstTgt::named("heya"))]
237  #[case("this::that[A]", InstTgt::path("this::that", "A"))]
238  fn test_component_id(#[case] input: &'static str, #[case] expected: InstTgt) -> Result<()> {
239    let (i, t) = instance(input)?;
240    assert_eq!(t, expected);
241    assert_eq!(i, "");
242    Ok(())
243  }
244
245  #[rstest]
246  #[case("single")]
247  #[case("<input>")]
248  fn test_path_negative(#[case] input: &str) -> Result<()> {
249    assert_err(&operation_path(input));
250    Ok(())
251  }
252
253  #[rstest]
254  #[case("comp::op", "comp::op")]
255  #[case("THIS::That", "THIS::That")]
256  #[allow(clippy::needless_pass_by_value)]
257  fn path_tester(#[case] input: &'static str, #[case] expected: &str) -> Result<()> {
258    let (i, t) = operation_path(input)?;
259    assert_eq!(t, expected);
260    assert_eq!(i, "");
261    Ok(())
262  }
263
264  #[rstest]
265  #[case("comp::op[FOO].foo", (InstTgt::path("comp::op","FOO"), InstPort::named("foo")))]
266  #[case("That.bar", (InstTgt::Named("That".to_owned()), InstPort::named("bar")))]
267  #[case("<>.input", (InstTgt::Default, InstPort::named("input")))]
268  #[case("input.foo", (InstTgt::named("input"), InstPort::named("foo")))]
269  #[case("ref.foo", (InstTgt::named("ref"), InstPort::named("foo")))]
270  #[case("<>.foo", (InstTgt::Default, InstPort::named("foo")))]
271  fn connection_target_expression_tester(
272    #[case] input: &'static str,
273    #[case] expected: (InstTgt, InstancePort),
274  ) -> Result<()> {
275    let (i, t) = connection_target_expression(input)?;
276    assert_eq!(t, expected);
277    assert_eq!(i, "");
278    Ok(())
279  }
280
281  #[rstest]
282  #[case("foo", InstancePort::named("foo"))]
283  #[case("foo.hey", InstancePort::path("foo", vec!["hey".to_owned()]))]
284  #[case("foo.hey.0.this", InstancePort::path("foo", vec!["hey".to_owned(),"0".to_owned(),"this".to_owned()]))]
285  #[case("input.\"Raw String Field #\"", InstancePort::path("input", vec!["Raw String Field #".to_owned()]))]
286  fn test_instance_port(#[case] input: &'static str, #[case] expected: InstancePort) -> Result<()> {
287    let (i, actual) = instance_port(input)?;
288
289    assert_eq!(expected, actual);
290    assert_eq!(i, "");
291    Ok(())
292  }
293
294  #[rstest]
295  #[case("comp::op[INLINE].foo -> <>.output", ((InstTgt::path("comp::op","INLINE"), "foo"),(InstTgt::Default, "output")))]
296  #[case("<> -> ref1.port", ((InstTgt::Default, "port"),(InstTgt::named("ref1"), "port")))]
297  #[case("ref1.in -> ref2.out", ((InstTgt::named("ref1"), "in"),(InstTgt::named("ref2"), "out")))]
298  #[case("<>.in->ref2.out", ((InstTgt::Default, "in"),(InstTgt::named("ref2"), "out")))]
299  #[case("ref1.in-><>.out", ((InstTgt::named("ref1"), "in"),(InstTgt::Default, "out")))]
300  #[case("ref1.port-><>", ((InstTgt::named("ref1"), "port"),(InstTgt::Default, "port")))]
301  #[case("<> -> ref1.port", ((InstTgt::Default, "port"),(InstTgt::named("ref1"), "port")))]
302  #[case("<> -> test::reverse[A].input",((InstTgt::Default, "input"),(InstTgt::path("test::reverse","A"), "input")))]
303  #[case("<>.anything -> drop",((InstTgt::Default, "anything"),(InstTgt::Null(None), InstancePort::None)))]
304  fn connection_parts(
305    #[case] input: &'static str,
306    #[case] expected: ((InstTgt, impl Into<InstancePort>), (InstTgt, impl Into<InstancePort>)),
307  ) -> Result<()> {
308    let (i, t) = connection_expression_sequence(input)?;
309    let expected = FlowExpression::ConnectionExpression(Box::new(CE::new(
310      CTE::new(expected.0 .0, expected.0 .1.into()),
311      CTE::new(expected.1 .0, expected.1 .1.into()),
312    )));
313
314    assert_eq!(t, expected);
315    assert_eq!(i, "");
316    Ok(())
317  }
318
319  fn assert_err<O, E>(item: &Result<O, E>)
320  where
321    E: std::fmt::Debug,
322    O: std::fmt::Debug,
323  {
324    if !item.is_err() {
325      panic!("Expected error, got {:?}", item);
326    } else {
327    }
328  }
329
330  fn flow_expr_conn(from_tgt: InstTgt, from_port: InstPort, to_tgt: InstTgt, to_port: InstPort) -> FE {
331    FE::connection(CE::new(CTE::new(from_tgt, from_port), CTE::new(to_tgt, to_port)))
332  }
333
334  fn flow_block<const K: usize>(seed: u64, hops: impl FnOnce() -> [(InstTgt, InstPort); K]) -> FE {
335    set_seed(seed);
336    let mut connections = Vec::new();
337    let mut last_hop: Option<(InstanceTarget, InstancePort)> = None;
338    let hops = hops();
339    for hop in hops {
340      if let Some(last) = last_hop.take() {
341        last_hop = Some(hop.clone());
342        connections.push(FE::connection(CE::new(
343          CTE::new(last.0, last.1),
344          CTE::new(hop.0, hop.1),
345        )));
346      } else {
347        last_hop = Some(hop);
348      }
349    }
350    set_seed(seed);
351    FE::block(BlockExpression::new(connections))
352  }
353
354  mod rng_limited {
355    use pretty_assertions::assert_eq;
356
357    use super::*;
358
359    #[rstest]
360    #[case(
361      "comp::op[INLINE].foo -> <>.output",
362      flow_expr_conn(
363        InstTgt::path("comp::op", "INLINE"),
364        InstPort::named("foo"),
365        InstTgt::Default,
366        InstPort::named("output")
367      )
368    )]
369    #[case(
370      "this.output.field -> <>.output",
371      flow_expr_conn(InstTgt::named("this"), InstPort::path("output",vec!["field".to_owned()]), InstTgt::Default, InstPort::named("output"))
372    )]
373    #[case(
374      "this.output.field.other.0 -> <>.output",
375      flow_expr_conn(InstTgt::named("this"), InstPort::path("output",vec!["field".to_owned(),"other".to_owned(), "0".to_owned()]), InstTgt::Default, InstPort::named("output"))
376    )]
377    #[case(
378      "this.output.\"field\" -> <>.output",
379      flow_expr_conn(InstTgt::named("this"), InstPort::path("output",vec!["field".to_owned()]), InstTgt::Default, InstPort::named("output"))
380    )]
381    #[case(
382      "this.output.\"field with spaces and \\\" quotes and symbols ,*|#\" -> <>.output",
383      flow_expr_conn(InstTgt::named("this"), InstPort::path("output",vec!["field with spaces and \" quotes and symbols ,*|#".to_owned()]), InstTgt::Default, InstPort::named("output"))
384    )]
385    #[case(
386      "this -> that",
387      flow_expr_conn(InstTgt::named("this"), InstPort::None, InstTgt::named("that"), InstPort::None)
388    )]
389    #[case(
390      "this -> that -> another",
391      flow_block(0,||[
392        (InstTgt::named("this"), InstPort::None),
393        (InstTgt::named("that"), InstPort::None),
394        (InstTgt::named("another"), InstPort::None)
395      ])
396    )]
397    #[case(
398      "<> -> test::reverse -> test::uppercase -> <>",
399      flow_block(0,||[
400        (InstTgt::Input, InstPort::None),
401        (InstTgt::generated_path("test::reverse"), InstPort::None),
402        (InstTgt::generated_path("test::uppercase"), InstPort::None),
403        (InstTgt::Output, InstPort::None)
404        ])
405    )]
406    #[case(
407      "test::in -> test::middle -> test::out",
408      flow_block(0,||[
409        (InstTgt::generated_path("test::in"), InstPort::None),
410        (InstTgt::generated_path("test::middle"), InstPort::None),
411        (InstTgt::generated_path("test::out"), InstPort::None),
412        ])
413    )]
414
415    fn test_flow_expression(#[case] input: &'static str, #[case] expected: FE) -> Result<()> {
416      set_seed(0);
417      let (t, actual) = flow_expression(input)?;
418      println!("expected: {:?}", expected);
419      println!("actual: {:?}", actual);
420      assert_eq!(actual, expected);
421      match actual {
422        FlowExpression::ConnectionExpression(_) => {
423          // no extra tests
424        }
425        FlowExpression::BlockExpression(block) => {
426          // backup tests to ensure that the flow has the right... flow.
427          let mut last: Option<&ConnectionExpression> = None;
428          let inner = block.inner();
429
430          for expr in inner {
431            let this_con = expr.as_connection().unwrap();
432            if let Some(last) = last {
433              // assert the last downstream is the current's upstream
434              assert_eq!(last.to(), this_con.from());
435              // assert that each hop has a different ID
436              assert_ne!(last.to().instance(), this_con.to().instance());
437            }
438            last = Some(this_con);
439          }
440        }
441      }
442
443      assert_eq!(t, "");
444      Ok(())
445    }
446  }
447}