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#[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}