Skip to main content

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![
42            Label::primary((), err.location.offset..err.location.offset).with_message(label_msg),
43        ])
44}
45
46#[cfg(test)]
47mod tests {
48    use pretty_assertions::assert_eq;
49
50    use super::file_parser;
51    use crate::parse::{
52        ast::{Line, Span},
53        token::*,
54    };
55
56    mod parser {
57        use super::{super::parser::g_code::*, 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_field_with_explicit_plus_sign() {
100            // Just in case some dialect emits an explicit + on positive values
101            let gcode = "X+1.5 Y+0";
102            let parsed = file_parser(gcode).unwrap();
103            let fields: Vec<_> = parsed.iter_fields().collect();
104            assert_eq!(fields[0].letters, "X");
105            assert_eq!(fields[1].letters, "Y");
106        }
107
108        #[test]
109        fn parses_dollar_prefixed_field() {
110            // https://github.com/sameer/svg2gcode/issues/82
111            // LinuxCNC spindle selector syntax: M3 $1 (start spindle 1)
112            let gcode = "M3 $1";
113            let parsed = file_parser(gcode).unwrap();
114            let fields: Vec<_> = parsed.iter_fields().collect();
115            assert_eq!(fields[1].letters, "$");
116        }
117
118        #[test]
119        fn parses_fields_with_trailing_whitespace() {
120            let gcode = "G0 X1 ";
121            file_parser(gcode).unwrap();
122        }
123
124        #[test]
125        fn parses_fields_with_leading_whitespace() {
126            let gcode = " G0 X1";
127            line(gcode).unwrap();
128        }
129
130        #[test]
131        fn parses_field_followed_by_inline_comment() {
132            let gcode = "M1 ()";
133            line(gcode).unwrap();
134        }
135
136        #[test]
137        fn validates_checksums() {
138            let gcode = r#"N0 M106*36
139N1 G28*18
140N2 M107*39"#;
141            let parsed = file_parser(gcode).unwrap();
142            for (line, checksum) in parsed.iter().zip(&[36u8, 18u8, 39u8]) {
143                assert_eq!(line.compute_checksum(), *checksum);
144                assert_eq!(line.validate_checksum(), Some(Ok(())));
145            }
146        }
147
148        #[test]
149        fn checksum_of_empty_line_is_zero() {
150            let gcode = "*0";
151            let parsed = file_parser(gcode).unwrap();
152            assert_eq!(parsed.iter().next().unwrap().compute_checksum(), 0u8);
153        }
154
155        #[test]
156        fn checksum_of_line_with_inline_comments_is_correct() {
157            let gcode = "(inline)G0 X0 (inline) (inline) Y0(inline)";
158            let parsed = file_parser(gcode).unwrap();
159            assert_eq!(
160                parsed
161                    .iter()
162                    .next()
163                    .unwrap()
164                    .iter_bytes()
165                    .copied()
166                    .collect::<Vec<u8>>(),
167                gcode.as_bytes()
168            );
169            assert_eq!(
170                parsed.iter().next().unwrap().compute_checksum(),
171                gcode.as_bytes().iter().fold(0u8, |acc, x| acc ^ x)
172            );
173        }
174
175        #[test]
176        fn checksum_of_line_with_comment_is_correct() {
177            let gcode = "(inline)G0 X0 (inline) (inline) Y0(inline);eolcomment";
178            let parsed = file_parser(gcode).unwrap();
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 checksum_of_line_with_checkum_and_comment_is_correct() {
193            let gcode = "(inline)G0 X0 (inline) (inline) Y0(inline)*118;eolcomment";
194            let parsed = file_parser(gcode).unwrap();
195            assert_eq!(
196                parsed.iter().next().unwrap().validate_checksum(),
197                Some(Ok(()))
198            );
199            assert_eq!(
200                parsed.iter().next().unwrap().compute_checksum(),
201                gcode
202                    .split('*')
203                    .next()
204                    .unwrap()
205                    .as_bytes()
206                    .iter()
207                    .fold(0u8, |acc, x| acc ^ x)
208            );
209        }
210
211        #[test]
212        fn inline_comment_is_parsed() {
213            let gcode = "(comment)";
214            let parsed = file_parser(gcode).unwrap();
215            assert_eq!(
216                *parsed.iter().next().unwrap(),
217                Line {
218                    line_components: vec![LineComponent {
219                        inline_comment: Some(InlineComment {
220                            inner: "(comment)",
221                            pos: 0
222                        }),
223                        ..Default::default()
224                    }],
225                    checksum: None,
226                    comment: None,
227                    span: Span(0, gcode.len())
228                }
229            );
230        }
231    }
232
233    mod lexer {
234        use super::{super::parser::g_code::*, assert_eq, *};
235
236        #[test]
237        fn escaped_quotes_are_lexed() {
238            let gcode = r#""""Testing""""#;
239            assert_eq!(string(gcode), Ok(gcode));
240        }
241
242        #[test]
243        fn comment_is_lexed() {
244            let gcode = ";Comment";
245            assert_eq!(
246                comment(gcode),
247                Ok(Comment {
248                    inner: gcode,
249                    pos: 0
250                })
251            );
252        }
253
254        #[test]
255        fn letter_is_lexed() {
256            let gcode = "A";
257            assert_eq!(letter(gcode), Ok(gcode),)
258        }
259
260        #[test]
261        fn letters_are_lexed() {
262            let gcode = "ABCD";
263            assert_eq!(letters(gcode), Ok(gcode),)
264        }
265
266        #[test]
267        fn integer_is_lexed() {
268            let gcode = "1234567890";
269            assert_eq!(integer(gcode), Ok(gcode),)
270        }
271
272        #[test]
273        fn dot_is_lexed() {
274            let gcode = ".";
275            assert_eq!(dot(gcode), Ok(gcode),)
276        }
277
278        #[test]
279        fn star_is_lexed() {
280            let gcode = "*";
281            assert_eq!(star(gcode), Ok(gcode),)
282        }
283
284        #[test]
285        fn minus_is_lexed() {
286            let gcode = "-";
287            assert_eq!(minus(gcode), Ok(gcode),)
288        }
289
290        #[test]
291        fn percent_is_lexed() {
292            let gcode = "%";
293            assert_eq!(percent(gcode), Ok(Percent { pos: 0 }),)
294        }
295
296        #[test]
297        fn newline_is_lexed() {
298            let gcode = "\n";
299            assert_eq!(newline(gcode), Ok(Newline { pos: 0 }),)
300        }
301
302        #[test]
303        fn inline_comment_is_lexed() {
304            let gcode = "(Comment)";
305            assert_eq!(
306                inline_comment(gcode),
307                Ok(InlineComment {
308                    pos: 0,
309                    inner: gcode
310                }),
311            )
312        }
313
314        #[test]
315        fn non_ascii_in_string_returns_unexpected_character_error() {
316            assert!(string(r#""§""#).is_err());
317        }
318
319        #[test]
320        fn non_ascii_in_comment_returns_unexpected_character_error() {
321            assert!(comment(";§").is_err());
322        }
323
324        #[test]
325        fn non_ascii_in_inline_comment_returns_unexpected_character_error() {
326            assert!(inline_comment("(§)").is_err());
327        }
328
329        #[test]
330        fn unterminated_quote_returns_unexpected_eof_error() {
331            assert!(string("\"x").is_err());
332        }
333
334        #[test]
335        fn unterminated_inline_comment_returns_unexpected_eof_error() {
336            assert!(inline_comment("(x").is_err());
337        }
338
339        #[test]
340        fn unterminated_inline_comment_followed_by_newline_returns_unexpected_newline_error() {
341            assert!(inline_comment("(x\n)").is_err());
342        }
343    }
344}