async_gcode/
parser.rs

1//! The ebnf representatio following [https://bottlecaps.de/rr/ui]'s syntax
2//! ```ebnf
3//! line       ::= '/'? ( [Nn] [0-9]+ )?
4//!                ( [a-zA-Z] real_value? | '#' real_value '=' real_value? | '(' [^)] ')' )*
5//!                ( '*' [0-9]+ /* 0 to 255 */ )?
6//!                ( ';' [^\n]* )? '\n'
7//! real_value ::= '#'* ( real_number
8//!                     | '"' [^"] '"'
9//!                     | 'atan' expression '/' expression
10//!                     | ( 'abs' | 'acos' | 'asin' | 'cos' | 'exp' | 'fix' | 'fup' | 'ln' | 'round' | 'sin' | 'sqrt' | 'tan' ) expression )
11//! expression ::= '[' real_value ( ( '**' | '/' | 'mod' | '*' | 'and' | 'xor' | '-' | 'or' | '+' ) real_value )* ']'
12//! real_number ::= ( '+' | '-' )? ( [0-9]+ ( '.' [0-9]* )? | '.' [0-9]+ )
13//! ```
14//!
15#[cfg(all(not(feature = "std"), feature = "parse-comments"))]
16use alloc::string::String;
17#[cfg(all(not(feature = "std"), any(feature = "parse-comments")))]
18use alloc::vec::Vec;
19
20mod values;
21
22#[cfg(feature = "parse-expressions")]
23mod expressions;
24
25#[cfg(test)]
26mod test;
27
28use futures::{Stream, StreamExt};
29
30use crate::{
31    stream::{MyTryStreamExt, PushBackable},
32    types::{Comment, ParseResult},
33    utils::skip_whitespaces,
34    Error, GCode,
35};
36
37use values::parse_number;
38
39#[cfg(not(feature = "parse-expressions"))]
40use values::parse_real_value;
41
42#[cfg(feature = "parse-expressions")]
43use expressions::parse_real_value;
44
45#[derive(PartialEq, Debug, Clone, Copy)]
46enum AsyncParserState {
47    Start(bool),
48    LineNumberOrSegment,
49    Segment,
50    ErrorRecovery,
51    #[cfg(all(feature = "parse-trailing-comment", feature = "parse-checksum"))]
52    EoLOrTrailingComment,
53    #[cfg(any(feature = "parse-trailing-comment", feature = "parse-checksum"))]
54    EndOfLine,
55}
56
57#[cfg(all(feature = "parse-trailing-comment", not(feature = "parse-comments")))]
58async fn parse_eol_comment<S, E>(input: &mut S) -> Option<Result<Comment, E>>
59where
60    S: Stream<Item = Result<u8, E>> + Unpin + PushBackable<Item = u8>,
61{
62    loop {
63        let b = match input.next().await? {
64            Ok(o) => o,
65            Err(e) => return Some(Err(e)),
66        };
67        match b {
68            b'\r' | b'\n' => {
69                input.push_back(b);
70                break Some(Ok(()));
71            }
72            _ => {}
73        }
74    }
75}
76
77#[cfg(all(feature = "parse-trailing-comment", feature = "parse-comments"))]
78async fn parse_eol_comment<S, E>(input: &mut S) -> Option<ParseResult<Comment, E>>
79where
80    S: Stream<Item = Result<u8, E>> + Unpin + PushBackable<Item = u8>,
81{
82    let mut v = Vec::new();
83    loop {
84        let b = try_result!(input.next());
85        match b {
86            b'\r' | b'\n' => {
87                input.push_back(b);
88                break Some(match String::from_utf8(v) {
89                    Ok(s) => ParseResult::Ok(s),
90                    Err(_) => Error::InvalidUTF8String.into(),
91                });
92            }
93            b => v.push(b),
94        }
95    }
96}
97
98#[cfg(not(feature = "parse-comments"))]
99async fn parse_inline_comment<S, E>(input: &mut S) -> Option<ParseResult<Comment, E>>
100where
101    S: Stream<Item = Result<u8, E>> + Unpin + PushBackable<Item = u8>,
102{
103    loop {
104        match try_result!(input.next()) {
105            b'\\' => {
106                try_result!(input.next());
107            }
108            b'(' => break Some(Error::UnexpectedByte(b'(').into()),
109            b')' => break Some(ParseResult::Ok(())),
110            _ => {}
111        }
112    }
113}
114
115#[cfg(feature = "parse-comments")]
116async fn parse_inline_comment<S, E>(input: &mut S) -> Option<ParseResult<Comment, E>>
117where
118    S: Stream<Item = Result<u8, E>> + Unpin + PushBackable<Item = u8>,
119{
120    let mut v = Vec::new();
121    loop {
122        let b = try_result!(input.next());
123        match b {
124            b'\\' => {
125                v.push(try_result!(input.next()));
126            }
127            b'(' => break Some(Error::UnexpectedByte(b'(').into()),
128            b')' => {
129                break Some(match String::from_utf8(v) {
130                    Ok(s) => ParseResult::Ok(s),
131                    Err(_) => Error::InvalidUTF8String.into(),
132                })
133            }
134            b => v.push(b),
135        }
136    }
137}
138
139// use a different struct to compute checksum
140#[cfg(not(feature = "parse-checksum"))]
141use crate::stream::pushback::PushBack;
142#[cfg(feature = "parse-checksum")]
143type PushBack<T> = crate::stream::xorsum_pushback::XorSumPushBack<T>;
144
145async fn parse_eol<S, E>(
146    state: &mut AsyncParserState,
147    input: &mut PushBack<S>,
148) -> Option<ParseResult<GCode, E>>
149where
150    S: Stream<Item = Result<u8, E>> + Unpin,
151{
152    Some(loop {
153        let b = try_result!(input.next());
154        match b {
155            b'\r' | b'\n' => {
156                *state = AsyncParserState::Start(true);
157                #[cfg(feature = "parse-checksum")]
158                {
159                    input.reset_sum(0);
160                }
161                break ParseResult::Ok(GCode::Execute);
162            }
163            b' ' => {}
164            b => break Error::UnexpectedByte(b).into(),
165        }
166    })
167}
168
169macro_rules! try_await {
170    ($input:expr) => {
171        match $input.await? {
172            ParseResult::Ok(ok) => ok,
173            ParseResult::Parsing(err) => break Err(err.into()),
174            ParseResult::Input(err) => break Err(err.into()),
175        }
176    };
177}
178
179macro_rules! try_await_result {
180    ($input:expr) => {
181        match $input.await? {
182            Ok(ok) => ok,
183            Err(err) => break Err(err.into()),
184        }
185    };
186}
187
188pub struct Parser<S, E>
189where
190    S: Stream<Item = Result<u8, E>> + Unpin,
191{
192    input: PushBack<S>,
193    state: AsyncParserState,
194}
195
196impl<S, E> Parser<S, E>
197where
198    S: Stream<Item = Result<u8, E>> + Unpin,
199    E: From<Error>,
200{
201    pub fn new(input: S) -> Self {
202        Self {
203            #[cfg(feature = "parse-checksum")]
204            input: input.xor_summed_push_backable(0),
205            #[cfg(not(feature = "parse-checksum"))]
206            input: input.push_backable(),
207            state: AsyncParserState::Start(true),
208        }
209    }
210    pub async fn next(&mut self) -> Option<Result<GCode, E>> {
211        let res = loop {
212            let b = match self.input.next().await? {
213                Ok(b) => b,
214                Err(err) => return Some(Err(err)),
215            };
216
217            // println!("{:?}: {:?}", self.state, char::from(b));
218            match self.state {
219                AsyncParserState::Start(ref mut first_byte) => match b {
220                    b'\n' => {
221                        self.input.push_back(b);
222                        break Ok(try_await!(parse_eol(&mut self.state, &mut self.input)));
223                    }
224                    b'/' if *first_byte => {
225                        self.state = AsyncParserState::LineNumberOrSegment;
226                        break Ok(GCode::BlockDelete);
227                    }
228                    b' ' => {
229                        *first_byte = false;
230                    }
231                    _ => {
232                        self.input.push_back(b);
233                        self.state = AsyncParserState::LineNumberOrSegment
234                    }
235                },
236                AsyncParserState::LineNumberOrSegment => match b.to_ascii_lowercase() {
237                    b'n' => {
238                        try_await_result!(skip_whitespaces(&mut self.input));
239                        let (n, ord) = try_await_result!(parse_number(&mut self.input));
240                        break if ord == 1 {
241                            let b = try_await_result!(self.input.next());
242                            Err(Error::UnexpectedByte(b).into())
243                        } else if ord > 10000 {
244                            Err(Error::NumberOverflow.into())
245                        } else {
246                            self.state = AsyncParserState::Segment;
247                            Ok(GCode::LineNumber(n))
248                        };
249                    }
250                    _ => {
251                        self.input.push_back(b);
252                        self.state = AsyncParserState::Segment;
253                    }
254                },
255                AsyncParserState::Segment => match b.to_ascii_lowercase() {
256                    b' ' => {}
257                    letter @ b'a'..=b'z' => {
258                        try_await_result!(skip_whitespaces(&mut self.input));
259                        let rv = try_await!(parse_real_value(&mut self.input));
260                        // println!("word({:?}, {:?})", letter as char, rv);
261                        break Ok(GCode::Word(letter.into(), rv));
262                    }
263                    b'\r' | b'\n' => {
264                        self.input.push_back(b);
265                        break Ok(try_await!(parse_eol(&mut self.state, &mut self.input)));
266                    }
267                    // param support feature
268                    #[cfg(feature = "parse-parameters")]
269                    b'#' => {
270                        try_await_result!(skip_whitespaces(&mut self.input));
271                        #[allow(clippy::match_single_binding)]
272                        let param_id = match try_await!(parse_real_value(&mut self.input)) {
273                            #[cfg(feature = "optional-value")]
274                            crate::RealValue::None => {
275                                let b = try_await_result!(self.input.next());
276                                break Err(Error::UnexpectedByte(b).into());
277                            }
278                            id => id,
279                        };
280                        // println!("param_id: {:?}", param_id);
281                        try_await_result!(skip_whitespaces(&mut self.input));
282                        let b = try_await_result!(self.input.next());
283                        if b'=' != b {
284                            break Err(Error::UnexpectedByte(b).into());
285                        }
286
287                        try_await_result!(skip_whitespaces(&mut self.input));
288                        let value = try_await!(parse_real_value(&mut self.input));
289                        // println!("param_id: {:?}", value);
290
291                        break Ok(GCode::ParameterSet(param_id, value));
292                    }
293                    // checksum support feature
294                    #[cfg(feature = "parse-checksum")]
295                    b'*' => {
296                        let sum = self.input.sum() ^ b'*';
297                        try_await_result!(skip_whitespaces(&mut self.input));
298                        let (n, _) = try_await_result!(parse_number(&mut self.input));
299                        // println!("{} {}", sum, n);
300                        if n >= 256 {
301                            break Err(Error::NumberOverflow.into());
302                        } else if (n as u8) != sum {
303                            break Err(Error::BadChecksum(sum).into());
304                        } else {
305                            try_await_result!(skip_whitespaces(&mut self.input));
306                            #[cfg(not(feature = "parse-trailing-comment"))]
307                            {
308                                self.state = AsyncParserState::EndOfLine;
309                            }
310                            #[cfg(feature = "parse-trailing-comment")]
311                            {
312                                self.state = AsyncParserState::EoLOrTrailingComment;
313                            }
314                        }
315                    }
316                    // comment support features
317                    #[cfg(not(feature = "parse-comments"))]
318                    b'(' => {
319                        try_await!(parse_inline_comment(&mut self.input));
320                    }
321                    #[cfg(feature = "parse-comments")]
322                    b'(' => {
323                        break Ok(GCode::Comment(try_await!(parse_inline_comment(
324                            &mut self.input
325                        ))));
326                    }
327                    #[cfg(all(
328                        feature = "parse-trailing-comment",
329                        not(feature = "parse-comments")
330                    ))]
331                    b';' => {
332                        try_await_result!(parse_eol_comment(&mut self.input));
333                        self.state = AsyncParserState::EndOfLine;
334                    }
335                    #[cfg(all(feature = "parse-trailing-comment", feature = "parse-comments"))]
336                    b';' => {
337                        let s = try_await!(parse_eol_comment(&mut self.input));
338                        self.state = AsyncParserState::EndOfLine;
339                        break Ok(GCode::Comment(s));
340                    }
341                    _ => break Err(Error::UnexpectedByte(b).into()),
342                },
343                #[cfg(all(
344                    feature = "parse-trailing-comment",
345                    not(feature = "parse-comments"),
346                    feature = "parse-checksum"
347                ))]
348                AsyncParserState::EoLOrTrailingComment => match b {
349                    b';' => try_await_result!(parse_eol_comment(&mut self.input)),
350                    _ => {
351                        self.input.push_back(b);
352                        break Ok(try_await!(parse_eol(&mut self.state, &mut self.input)));
353                    }
354                },
355                #[cfg(all(
356                    feature = "parse-trailing-comment",
357                    feature = "parse-comments",
358                    feature = "parse-checksum"
359                ))]
360                AsyncParserState::EoLOrTrailingComment => match b {
361                    b';' => {
362                        let s = try_await!(parse_eol_comment(&mut self.input));
363                        self.state = AsyncParserState::EndOfLine;
364                        break Ok(GCode::Comment(s));
365                    }
366                    _ => {
367                        self.input.push_back(b);
368                        break Ok(try_await!(parse_eol(&mut self.state, &mut self.input)));
369                    }
370                },
371                #[cfg(any(feature = "parse-trailing-comment", feature = "parse-checksum"))]
372                AsyncParserState::EndOfLine => {
373                    self.input.push_back(b);
374                    break Ok(try_await!(parse_eol(&mut self.state, &mut self.input)));
375                }
376                AsyncParserState::ErrorRecovery => match b {
377                    b'\r' | b'\n' => {
378                        self.input.push_back(b);
379                        break Ok(try_await!(parse_eol(&mut self.state, &mut self.input)));
380                    }
381                    _ => {}
382                },
383            }
384        };
385        // eprintln!("{}:{} {:?}", file!(), line!(), res);
386        if res.is_err() {
387            self.state = AsyncParserState::ErrorRecovery;
388        }
389        Some(res)
390    }
391}