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