gcode/
parser.rs

1use crate::{
2    buffers::{Buffers, DefaultBuffers},
3    lexer::{Lexer, Token, TokenType},
4    words::{Atom, Word, WordsOrComments},
5    Callbacks, Comment, GCode, Line, Mnemonic, Nop,
6};
7use core::{iter::Peekable, marker::PhantomData};
8
9/// Parse each [`GCode`] in some text, ignoring any errors that may occur or
10/// [`Comment`]s that are found.
11///
12/// This function is probably what you are looking for if you just want to read
13/// the [`GCode`] commands in a program. If more detailed information is needed,
14/// have a look at [`full_parse_with_callbacks()`].
15pub fn parse<'input>(src: &'input str) -> impl Iterator<Item = GCode> + 'input {
16    full_parse_with_callbacks(src, Nop).flat_map(|line| line.into_gcodes())
17}
18
19/// Parse each [`Line`] in some text, using the provided [`Callbacks`] when a
20/// parse error occurs that we can recover from.
21///
22/// Unlike [`parse()`], this function will also give you access to any comments
23/// and line numbers that are found, plus the location of the entire [`Line`]
24/// in its source text.
25pub fn full_parse_with_callbacks<'input, C: Callbacks + 'input>(
26    src: &'input str,
27    callbacks: C,
28) -> impl Iterator<Item = Line<'input>> + 'input {
29    let tokens = Lexer::new(src);
30    let atoms = WordsOrComments::new(tokens);
31    Lines::new(atoms, callbacks)
32}
33
34/// A parser for parsing g-code programs.
35#[derive(Debug)]
36pub struct Parser<'input, C, B = DefaultBuffers> {
37    // Explicitly instantiate Lines so Parser's type parameters don't expose
38    // internal details
39    lines: Lines<'input, WordsOrComments<'input, Lexer<'input>>, C, B>,
40}
41
42impl<'input, C, B> Parser<'input, C, B> {
43    /// Create a new [`Parser`] from some source text and a set of
44    /// [`Callbacks`].
45    pub fn new(src: &'input str, callbacks: C) -> Self {
46        let tokens = Lexer::new(src);
47        let atoms = WordsOrComments::new(tokens);
48        let lines = Lines::new(atoms, callbacks);
49        Parser { lines }
50    }
51}
52
53impl<'input, B> From<&'input str> for Parser<'input, Nop, B> {
54    fn from(src: &'input str) -> Self { Parser::new(src, Nop) }
55}
56
57impl<'input, C: Callbacks, B: Buffers<'input>> Iterator
58    for Parser<'input, C, B>
59{
60    type Item = Line<'input, B>;
61
62    fn next(&mut self) -> Option<Self::Item> { self.lines.next() }
63}
64
65#[derive(Debug)]
66struct Lines<'input, I, C, B>
67where
68    I: Iterator<Item = Atom<'input>>,
69{
70    atoms: Peekable<I>,
71    callbacks: C,
72    last_gcode_type: Option<Word>,
73    _buffers: PhantomData<B>,
74}
75
76impl<'input, I, C, B> Lines<'input, I, C, B>
77where
78    I: Iterator<Item = Atom<'input>>,
79{
80    fn new(atoms: I, callbacks: C) -> Self {
81        Lines {
82            atoms: atoms.peekable(),
83            callbacks,
84            last_gcode_type: None,
85            _buffers: PhantomData,
86        }
87    }
88}
89
90impl<'input, I, C, B> Lines<'input, I, C, B>
91where
92    I: Iterator<Item = Atom<'input>>,
93    C: Callbacks,
94    B: Buffers<'input>,
95{
96    fn handle_line_number(
97        &mut self,
98        word: Word,
99        line: &mut Line<'input, B>,
100        has_temp_gcode: bool,
101    ) {
102        if line.gcodes().is_empty()
103            && line.line_number().is_none()
104            && !has_temp_gcode
105        {
106            line.set_line_number(word);
107        } else {
108            self.callbacks.unexpected_line_number(word.value, word.span);
109        }
110    }
111
112    fn handle_arg(
113        &mut self,
114        word: Word,
115        line: &mut Line<'input, B>,
116        temp_gcode: &mut Option<GCode<B::Arguments>>,
117    ) {
118        if let Some(mnemonic) = Mnemonic::for_letter(word.letter) {
119            // we need to start another gcode. push the one we were building
120            // onto the line so we can start working on the next one
121            self.last_gcode_type = Some(word);
122            if let Some(completed) = temp_gcode.take() {
123                if let Err(e) = line.push_gcode(completed) {
124                    self.on_gcode_push_error(e.0);
125                }
126            }
127            *temp_gcode = Some(GCode::new_with_argument_buffer(
128                mnemonic,
129                word.value,
130                word.span,
131                B::Arguments::default(),
132            ));
133            return;
134        }
135
136        // we've got an argument, try adding it to the gcode we're building
137        if let Some(temp) = temp_gcode {
138            if let Err(e) = temp.push_argument(word) {
139                self.on_arg_push_error(&temp, e.0);
140            }
141            return;
142        }
143
144        // we haven't already started building a gcode, maybe the author elided
145        // the command ("G90") and wants to use the one from the last line?
146        match self.last_gcode_type {
147            Some(ty) => {
148                let mut new_gcode = GCode::new_with_argument_buffer(
149                    Mnemonic::for_letter(ty.letter).unwrap(),
150                    ty.value,
151                    ty.span,
152                    B::Arguments::default(),
153                );
154                if let Err(e) = new_gcode.push_argument(word) {
155                    self.on_arg_push_error(&new_gcode, e.0);
156                }
157                *temp_gcode = Some(new_gcode);
158            },
159            // oh well, you can't say we didn't try...
160            None => {
161                self.callbacks.argument_without_a_command(
162                    word.letter,
163                    word.value,
164                    word.span,
165                );
166            },
167        }
168    }
169
170    fn handle_broken_word(&mut self, token: Token<'_>) {
171        if token.kind == TokenType::Letter {
172            self.callbacks
173                .letter_without_a_number(token.value, token.span);
174        } else {
175            self.callbacks
176                .number_without_a_letter(token.value, token.span);
177        }
178    }
179
180    fn on_arg_push_error(&mut self, gcode: &GCode<B::Arguments>, arg: Word) {
181        self.callbacks.gcode_argument_buffer_overflowed(
182            gcode.mnemonic(),
183            gcode.major_number(),
184            gcode.minor_number(),
185            arg,
186        );
187    }
188
189    fn on_comment_push_error(&mut self, comment: Comment<'_>) {
190        self.callbacks.comment_buffer_overflow(comment);
191    }
192
193    fn on_gcode_push_error(&mut self, gcode: GCode<B::Arguments>) {
194        self.callbacks.gcode_buffer_overflowed(
195            gcode.mnemonic(),
196            gcode.major_number(),
197            gcode.minor_number(),
198            gcode.arguments(),
199            gcode.span(),
200        );
201    }
202
203    fn next_line_number(&mut self) -> Option<usize> {
204        self.atoms.peek().map(|a| a.span().line)
205    }
206}
207
208impl<'input, I, C, B> Iterator for Lines<'input, I, C, B>
209where
210    I: Iterator<Item = Atom<'input>> + 'input,
211    C: Callbacks,
212    B: Buffers<'input>,
213{
214    type Item = Line<'input, B>;
215
216    fn next(&mut self) -> Option<Self::Item> {
217        let mut line = Line::default();
218        // we need a scratch space for the gcode we're in the middle of
219        // constructing
220        let mut temp_gcode = None;
221
222        while let Some(next_line) = self.next_line_number() {
223            if !line.is_empty() && next_line != line.span().line {
224                // we've started the next line
225                break;
226            }
227
228            match self.atoms.next().expect("unreachable") {
229                Atom::Unknown(token) => {
230                    self.callbacks.unknown_content(token.value, token.span)
231                },
232                Atom::Comment(comment) => {
233                    if let Err(e) = line.push_comment(comment) {
234                        self.on_comment_push_error(e.0);
235                    }
236                },
237                // line numbers are annoying, so handle them separately
238                Atom::Word(word) if word.letter.to_ascii_lowercase() == 'n' => {
239                    self.handle_line_number(
240                        word,
241                        &mut line,
242                        temp_gcode.is_some(),
243                    );
244                },
245                Atom::Word(word) => {
246                    self.handle_arg(word, &mut line, &mut temp_gcode)
247                },
248                Atom::BrokenWord(token) => self.handle_broken_word(token),
249            }
250        }
251
252        if let Some(gcode) = temp_gcode.take() {
253            if let Err(e) = line.push_gcode(gcode) {
254                self.on_gcode_push_error(e.0);
255            }
256        }
257
258        if line.is_empty() {
259            None
260        } else {
261            Some(line)
262        }
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use crate::Span;
270    use arrayvec::ArrayVec;
271    use std::{prelude::v1::*, sync::Mutex};
272
273    #[derive(Debug)]
274    struct MockCallbacks<'a> {
275        unexpected_line_number: &'a Mutex<Vec<(f32, Span)>>,
276    }
277
278    impl<'a> Callbacks for MockCallbacks<'a> {
279        fn unexpected_line_number(&mut self, line_number: f32, span: Span) {
280            self.unexpected_line_number
281                .lock()
282                .unwrap()
283                .push((line_number, span));
284        }
285    }
286
287    #[derive(Debug, Copy, Clone, PartialEq)]
288    enum BigBuffers {}
289
290    impl<'input> Buffers<'input> for BigBuffers {
291        type Arguments = ArrayVec<[Word; 16]>;
292        type Commands = ArrayVec<[GCode<Self::Arguments>; 16]>;
293        type Comments = ArrayVec<[Comment<'input>; 16]>;
294    }
295
296    fn parse(
297        src: &str,
298    ) -> Lines<'_, impl Iterator<Item = Atom<'_>>, Nop, BigBuffers> {
299        let tokens = Lexer::new(src);
300        let atoms = WordsOrComments::new(tokens);
301        Lines::new(atoms, Nop)
302    }
303
304    #[test]
305    fn we_can_parse_a_comment() {
306        let src = "(this is a comment)";
307        let got: Vec<_> = parse(src).collect();
308
309        assert_eq!(got.len(), 1);
310        let line = &got[0];
311        assert_eq!(line.comments().len(), 1);
312        assert_eq!(line.gcodes().len(), 0);
313        assert_eq!(line.span(), Span::new(0, src.len(), 0));
314    }
315
316    #[test]
317    fn line_numbers() {
318        let src = "N42";
319        let got: Vec<_> = parse(src).collect();
320
321        assert_eq!(got.len(), 1);
322        let line = &got[0];
323        assert_eq!(line.comments().len(), 0);
324        assert_eq!(line.gcodes().len(), 0);
325        let span = Span::new(0, src.len(), 0);
326        assert_eq!(
327            line.line_number(),
328            Some(Word {
329                letter: 'N',
330                value: 42.0,
331                span
332            })
333        );
334        assert_eq!(line.span(), span);
335    }
336
337    #[test]
338    fn line_numbers_after_the_start_are_an_error() {
339        let src = "G90 N42";
340        let unexpected_line_number = Default::default();
341        let got: Vec<_> = full_parse_with_callbacks(
342            src,
343            MockCallbacks {
344                unexpected_line_number: &unexpected_line_number,
345            },
346        )
347        .collect();
348
349        assert_eq!(got.len(), 1);
350        assert!(got[0].line_number().is_none());
351        let unexpected_line_number = unexpected_line_number.lock().unwrap();
352        assert_eq!(unexpected_line_number.len(), 1);
353        assert_eq!(unexpected_line_number[0].0, 42.0);
354    }
355
356    #[test]
357    fn parse_g90() {
358        let src = "G90";
359        let got: Vec<_> = parse(src).collect();
360
361        assert_eq!(got.len(), 1);
362        let line = &got[0];
363        assert_eq!(line.gcodes().len(), 1);
364        let g90 = &line.gcodes()[0];
365        assert_eq!(g90.major_number(), 90);
366        assert_eq!(g90.minor_number(), 0);
367        assert_eq!(g90.arguments().len(), 0);
368    }
369
370    #[test]
371    fn parse_command_with_arguments() {
372        let src = "G01X5 Y-20";
373        let should_be =
374            GCode::new(Mnemonic::General, 1.0, Span::new(0, src.len(), 0))
375                .with_argument(Word {
376                    letter: 'X',
377                    value: 5.0,
378                    span: Span::new(3, 5, 0),
379                })
380                .with_argument(Word {
381                    letter: 'Y',
382                    value: -20.0,
383                    span: Span::new(6, 10, 0),
384                });
385
386        let got: Vec<_> = parse(src).collect();
387
388        assert_eq!(got.len(), 1);
389        let line = &got[0];
390        assert_eq!(line.gcodes().len(), 1);
391        let g01 = &line.gcodes()[0];
392        assert_eq!(g01, &should_be);
393    }
394
395    #[test]
396    fn multiple_commands_on_the_same_line() {
397        let src = "G01 X5 G90 (comment) G91 M10\nG01";
398
399        let got: Vec<_> = parse(src).collect();
400
401        assert_eq!(got.len(), 2);
402        assert_eq!(got[0].gcodes().len(), 4);
403        assert_eq!(got[0].comments().len(), 1);
404        assert_eq!(got[1].gcodes().len(), 1);
405    }
406
407    /// I wasn't sure if the `#[derive(Serialize)]` would work given we use
408    /// `B::Comments`, which would borrow from the original source.
409    #[test]
410    #[cfg(feature = "serde-1")]
411    fn you_can_actually_serialize_lines() {
412        let src = "G01 X5 G90 (comment) G91 M10\nG01\n";
413        let line = parse(src).next().unwrap();
414
415        fn assert_serializable<S: serde::Serialize>(_: &S) {}
416        fn assert_deserializable<'de, D: serde::Deserialize<'de>>() {}
417
418        assert_serializable(&line);
419        assert_deserializable::<Line<'_>>();
420    }
421
422    /// For some reason we were parsing the G90, then an empty G01 and the
423    /// actual G01.
424    #[test]
425    #[ignore]
426    fn funny_bug_in_crate_example() {
427        let src = "G90 \n G01 X50.0 Y-10";
428        let expected = vec![
429            GCode::new(Mnemonic::General, 90.0, Span::PLACEHOLDER),
430            GCode::new(Mnemonic::General, 1.0, Span::PLACEHOLDER)
431                .with_argument(Word::new('X', 50.0, Span::PLACEHOLDER))
432                .with_argument(Word::new('Y', -10.0, Span::PLACEHOLDER)),
433        ];
434
435        let got: Vec<_> = crate::parse(src).collect();
436
437        assert_eq!(got, expected);
438    }
439}