g_code/parse/
mod.rs

1#[cfg(feature = "codespan_helpers")]
2use codespan_reporting::diagnostic::{Diagnostic as CodespanDiagnostic, Label};
3
4mod parser;
5pub use parser::g_code::{file_parser, snippet_parser};
6pub mod ast;
7#[cfg(feature = "binary")]
8pub mod compact;
9pub mod token;
10
11pub type ParseError = peg::error::ParseError<peg::str::LineCol>;
12#[cfg(feature = "codespan_helpers")]
13pub type Diagnostic = CodespanDiagnostic<()>;
14
15/// Convenience function for converting a parsing error
16/// into a [codespan_reporting::diagnostic::Diagnostic] for displaying to a user.
17#[cfg(feature = "codespan_helpers")]
18pub fn into_diagnostic(err: &ParseError) -> Diagnostic {
19    let expected_count = err.expected.tokens().count();
20    let label_msg = if expected_count == 0 {
21        "unclear cause".to_string()
22    } else if expected_count == 1 {
23        format!("expected {}", err.expected.tokens().next().unwrap())
24    } else {
25        let tokens = {
26            let mut tokens = err.expected.tokens().collect::<Vec<_>>();
27            tokens.sort_unstable();
28            tokens
29        };
30        let mut acc = "expected one of ".to_string();
31        for token in tokens.iter().take(expected_count - 1) {
32            acc += token;
33            acc += ", ";
34        }
35        acc += "or ";
36        acc += tokens.last().unwrap();
37        acc
38    };
39    Diagnostic::error()
40        .with_message("could not parse gcode")
41        .with_labels(vec![Label::primary(
42            (),
43            err.location.offset..err.location.offset,
44        )
45        .with_message(label_msg)])
46}
47
48#[cfg(test)]
49mod tests {
50    use super::file_parser;
51    use crate::parse::ast::{Line, Span};
52    use crate::parse::token::*;
53    use pretty_assertions::assert_eq;
54
55    mod parser {
56        use super::super::parser::g_code::*;
57        use super::{assert_eq, *};
58
59        #[test]
60        fn parses_svg2gcode_output() {
61            let gcode = include_str!("../../tests/vandy_commodores_logo.gcode");
62            file_parser(gcode).unwrap();
63        }
64
65        #[test]
66        fn parses_ncviewer_sample() {
67            let gcode = include_str!("../../tests/ncviewer_sample.gcode");
68            file_parser(gcode).unwrap();
69        }
70
71        #[test]
72        fn parses_field_with_string_value() {
73            let gcode = r#"S"MYROUTER""#;
74            file_parser(gcode).unwrap();
75        }
76
77        #[test]
78        fn parses_field_with_escaped_string_value() {
79            let gcode = r#"P"ABCxyz;"" 123""#;
80            file_parser(gcode).unwrap();
81        }
82
83        #[test]
84        fn parses_field_with_complex_string_value() {
85            let gcode = r#"
86                M587 S"MYROUTER" P"ABCxyz;"" 123" 
87                M587 S"MYROUTER" P"ABC'X'Y'Z;"" 123"
88            "#;
89            file_parser(gcode).unwrap();
90        }
91
92        #[test]
93        fn parses_fields_without_whitespace() {
94            let gcode = "G0X1Y0";
95            file_parser(gcode).unwrap();
96        }
97
98        #[test]
99        fn parses_fields_with_trailing_whitespace() {
100            let gcode = "G0 X1 ";
101            file_parser(gcode).unwrap();
102        }
103
104        #[test]
105        fn parses_fields_with_leading_whitespace() {
106            let gcode = " G0 X1";
107            line(gcode).unwrap();
108        }
109
110        #[test]
111        fn parses_field_followed_by_inline_comment() {
112            let gcode = "M1 ()";
113            line(gcode).unwrap();
114        }
115
116        #[test]
117        fn validates_checksums() {
118            let gcode = r#"N0 M106*36
119N1 G28*18
120N2 M107*39"#;
121            let parsed = file_parser(gcode).unwrap();
122            for (line, checksum) in parsed.iter().zip(&[36u8, 18u8, 39u8]) {
123                assert_eq!(line.compute_checksum(), *checksum);
124                assert_eq!(line.validate_checksum(), Some(Ok(())));
125            }
126        }
127
128        #[test]
129        fn checksum_of_empty_line_is_zero() {
130            let gcode = "*0";
131            let parsed = file_parser(gcode).unwrap();
132            assert_eq!(parsed.iter().next().unwrap().compute_checksum(), 0u8);
133        }
134
135        #[test]
136        fn checksum_of_line_with_inline_comments_is_correct() {
137            let gcode = "(inline)G0 X0 (inline) (inline) Y0(inline)";
138            let parsed = file_parser(gcode).unwrap();
139            assert_eq!(
140                parsed
141                    .iter()
142                    .next()
143                    .unwrap()
144                    .iter_bytes()
145                    .copied()
146                    .collect::<Vec<u8>>(),
147                gcode.as_bytes()
148            );
149            assert_eq!(
150                parsed.iter().next().unwrap().compute_checksum(),
151                gcode.as_bytes().iter().fold(0u8, |acc, x| acc ^ x)
152            );
153        }
154
155        #[test]
156        fn checksum_of_line_with_comment_is_correct() {
157            let gcode = "(inline)G0 X0 (inline) (inline) Y0(inline);eolcomment";
158            let parsed = file_parser(gcode).unwrap();
159            assert_eq!(
160                parsed.iter().next().unwrap().compute_checksum(),
161                gcode
162                    .split(';')
163                    .next()
164                    .unwrap()
165                    .as_bytes()
166                    .iter()
167                    .fold(0u8, |acc, x| acc ^ x)
168            );
169        }
170
171        #[test]
172        fn checksum_of_line_with_checkum_and_comment_is_correct() {
173            let gcode = "(inline)G0 X0 (inline) (inline) Y0(inline)*118;eolcomment";
174            let parsed = file_parser(gcode).unwrap();
175            assert_eq!(
176                parsed.iter().next().unwrap().validate_checksum(),
177                Some(Ok(()))
178            );
179            assert_eq!(
180                parsed.iter().next().unwrap().compute_checksum(),
181                gcode
182                    .split('*')
183                    .next()
184                    .unwrap()
185                    .as_bytes()
186                    .iter()
187                    .fold(0u8, |acc, x| acc ^ x)
188            );
189        }
190
191        #[test]
192        fn inline_comment_is_parsed() {
193            let gcode = "(comment)";
194            let parsed = file_parser(gcode).unwrap();
195            assert_eq!(
196                *parsed.iter().next().unwrap(),
197                Line {
198                    line_components: vec![LineComponent {
199                        inline_comment: Some(InlineComment {
200                            inner: "(comment)",
201                            pos: 0
202                        }),
203                        ..Default::default()
204                    }],
205                    checksum: None,
206                    comment: None,
207                    span: Span(0, gcode.len())
208                }
209            );
210        }
211    }
212
213    mod lexer {
214        use super::super::parser::g_code::*;
215        use super::{assert_eq, *};
216
217        #[test]
218        fn escaped_quotes_are_lexed() {
219            let gcode = r#""""Testing""""#;
220            assert_eq!(string(gcode), Ok(gcode));
221        }
222
223        #[test]
224        fn comment_is_lexed() {
225            let gcode = ";Comment";
226            assert_eq!(
227                comment(gcode),
228                Ok(Comment {
229                    inner: gcode,
230                    pos: 0
231                })
232            );
233        }
234
235        #[test]
236        fn letter_is_lexed() {
237            let gcode = "A";
238            assert_eq!(letter(gcode), Ok(gcode),)
239        }
240
241        #[test]
242        fn letters_are_lexed() {
243            let gcode = "ABCD";
244            assert_eq!(letters(gcode), Ok(gcode),)
245        }
246
247        #[test]
248        fn integer_is_lexed() {
249            let gcode = "1234567890";
250            assert_eq!(integer(gcode), Ok(gcode),)
251        }
252
253        #[test]
254        fn dot_is_lexed() {
255            let gcode = ".";
256            assert_eq!(dot(gcode), Ok(gcode),)
257        }
258
259        #[test]
260        fn star_is_lexed() {
261            let gcode = "*";
262            assert_eq!(star(gcode), Ok(gcode),)
263        }
264
265        #[test]
266        fn minus_is_lexed() {
267            let gcode = "-";
268            assert_eq!(minus(gcode), Ok(gcode),)
269        }
270
271        #[test]
272        fn percent_is_lexed() {
273            let gcode = "%";
274            assert_eq!(percent(gcode), Ok(Percent { pos: 0 }),)
275        }
276
277        #[test]
278        fn newline_is_lexed() {
279            let gcode = "\n";
280            assert_eq!(newline(gcode), Ok(Newline { pos: 0 }),)
281        }
282
283        #[test]
284        fn inline_comment_is_lexed() {
285            let gcode = "(Comment)";
286            assert_eq!(
287                inline_comment(gcode),
288                Ok(InlineComment {
289                    pos: 0,
290                    inner: gcode
291                }),
292            )
293        }
294
295        #[test]
296        fn non_ascii_in_string_returns_unexpected_character_error() {
297            assert!(string(r#""§""#).is_err());
298        }
299
300        #[test]
301        fn non_ascii_in_comment_returns_unexpected_character_error() {
302            assert!(comment(";§").is_err());
303        }
304
305        #[test]
306        fn non_ascii_in_inline_comment_returns_unexpected_character_error() {
307            assert!(inline_comment("(§)").is_err());
308        }
309
310        #[test]
311        fn unterminated_quote_returns_unexpected_eof_error() {
312            assert!(string("\"x").is_err());
313        }
314
315        #[test]
316        fn unterminated_inline_comment_returns_unexpected_eof_error() {
317            assert!(inline_comment("(x").is_err());
318        }
319
320        #[test]
321        fn unterminated_inline_comment_followed_by_newline_returns_unexpected_newline_error() {
322            assert!(inline_comment("(x\n)").is_err());
323        }
324    }
325}