diff --git a/gcode/src/lexer.rs b/gcode/src/lexer.rs index 69cb3ea0..a4f95171 100644 --- a/gcode/src/lexer.rs +++ b/gcode/src/lexer.rs @@ -5,6 +5,7 @@ pub(crate) enum TokenType { Letter, Number, Comment, + Newline, Unknown, } @@ -16,6 +17,8 @@ impl From for TokenType { TokenType::Number } else if c == '(' || c == ';' || c == ')' { TokenType::Comment + } else if c == '\n' { + TokenType::Newline } else { TokenType::Unknown } @@ -53,14 +56,14 @@ impl<'input> Lexer<'input> { { let start = self.current_position; let mut end = start; - let mut line_endings = 0; for letter in self.rest().chars() { if !predicate(letter) { break; } if letter == '\n' { - line_endings += 1; + // Newline defines the command to be complete. + break; } end += letter.len_utf8(); } @@ -69,7 +72,6 @@ impl<'input> Lexer<'input> { None } else { self.current_position = end; - self.current_line += line_endings; Some(&self.src[start..end]) } } @@ -175,6 +177,23 @@ impl<'input> Lexer<'input> { }, }) } + + fn tokenize_newline(&mut self) -> Option> { + let start = self.current_position; + let line = self.current_line; + let value = "\n"; + self.current_position += 1; + self.current_line += 1; + Some(Token { + kind: TokenType::Newline, + value, + span: Span { + start, + line, + end: start + 1, + }, + }) + } fn finished(&self) -> bool { self.current_position >= self.src.len() } @@ -219,6 +238,9 @@ impl<'input> Iterator for Lexer<'input> { TokenType::Number => { return Some(self.tokenize_number().expect(MSG)) }, + TokenType::Newline => { + return Some(self.tokenize_newline().expect(MSG)) + }, TokenType::Unknown => self.current_position += 1, } } @@ -253,11 +275,22 @@ mod tests { #[test] fn skip_whitespace() { - let mut lexer = Lexer::new(" \n\r\t "); + let mut lexer = Lexer::new(" \r\t "); lexer.skip_whitespace(); assert_eq!(lexer.current_position, lexer.src.len()); + assert_eq!(lexer.current_line, 0); + } + + #[test] + fn respect_newlines() { + let mut lexer = Lexer::new("\n\rM30garbage"); + + let token = lexer.tokenize_newline().unwrap(); + + assert_eq!(token.kind, TokenType::Newline); + assert_eq!(lexer.current_position, 1); assert_eq!(lexer.current_line, 1); } @@ -371,4 +404,44 @@ mod tests { assert_eq!(got.value, "+3.14"); } + + #[test] + fn two_multi() { + let mut lexer = Lexer::new("G0 X1\nG1 Y2"); + + let got = lexer.next().unwrap(); + assert_eq!(got.value, "G"); + assert_eq!(got.span.line, 0); + + let got = lexer.next().unwrap(); + assert_eq!(got.value, "0"); + assert_eq!(got.span.line, 0); + + let got = lexer.next().unwrap(); + assert_eq!(got.value, "X"); + assert_eq!(got.span.line, 0); + + let got = lexer.next().unwrap(); + assert_eq!(got.value, "1"); + assert_eq!(got.span.line, 0); + + let got = lexer.next().unwrap(); + assert_eq!(got.value, "\n"); + + let got = lexer.next().unwrap(); + assert_eq!(got.value, "G"); + assert_eq!(got.span.line, 1); + + let got = lexer.next().unwrap(); + assert_eq!(got.value, "1"); + assert_eq!(got.span.line, 1); + + let got = lexer.next().unwrap(); + assert_eq!(got.value, "Y"); + assert_eq!(got.span.line, 1); + + let got = lexer.next().unwrap(); + assert_eq!(got.value, "2"); + assert_eq!(got.span.line, 1); + } } diff --git a/gcode/src/lib.rs b/gcode/src/lib.rs index 5639de20..143c2123 100644 --- a/gcode/src/lib.rs +++ b/gcode/src/lib.rs @@ -164,7 +164,7 @@ unused_qualifications, unused_results, variant_size_differences, - intra_doc_link_resolution_failure, + rustdoc::broken_intra_doc_links, missing_docs )] #![cfg_attr(not(feature = "std"), no_std)] diff --git a/gcode/src/parser.rs b/gcode/src/parser.rs index c4dae9fd..54d5ec54 100644 --- a/gcode/src/parser.rs +++ b/gcode/src/parser.rs @@ -115,21 +115,29 @@ where line: &mut Line<'input, B>, temp_gcode: &mut Option>, ) { + // First, we check to see if the character is actually a new command. if let Some(mnemonic) = Mnemonic::for_letter(word.letter) { - // we need to start another gcode. push the one we were building - // onto the line so we can start working on the next one + // We need to start another gcode. + self.last_gcode_type = Some(word); + if let Some(completed) = temp_gcode.take() { + // We were already in progress building arguments for this code, and now we found + // a new command that effectively ends the previous command. + + // Push the g-code we were building onto the line so we can start working on the next one. if let Err(e) = line.push_gcode(completed) { self.on_gcode_push_error(e.0); } } + *temp_gcode = Some(GCode::new_with_argument_buffer( mnemonic, word.value, word.span, B::Arguments::default(), )); + return; } @@ -200,9 +208,6 @@ where ); } - fn next_line_number(&mut self) -> Option { - self.atoms.peek().map(|a| a.span().line) - } } impl<'input, I, C, B> Iterator for Lines<'input, I, C, B> @@ -219,13 +224,14 @@ where // constructing let mut temp_gcode = None; - while let Some(next_line) = self.next_line_number() { - if !line.is_empty() && next_line != line.span().line { - // we've started the next line - break; - } + if let None = self.atoms.peek() { + // There is nothing left in the file. :sad-face: + // This ends the parser's work. + return None; + } - match self.atoms.next().expect("unreachable") { + while let Some(atom) = self.atoms.next() { + match atom { Atom::Unknown(token) => { self.callbacks.unknown_content(token.value, token.span) }, @@ -234,6 +240,13 @@ where self.on_comment_push_error(e.0); } }, + Atom::Newline(_) => { + if !line.is_empty() || temp_gcode.is_some() { + // Newline ends the current command if there was something to parse. + break; + } + // Otherwise, the g-code had an empty line and we can ignore it. + }, // line numbers are annoying, so handle them separately Atom::Word(word) if word.letter.to_ascii_lowercase() == 'n' => { self.handle_line_number( @@ -255,11 +268,7 @@ where } } - if line.is_empty() { - None - } else { - Some(line) - } + Some(line) } } @@ -404,25 +413,7 @@ mod tests { assert_eq!(got[1].gcodes().len(), 1); } - /// I wasn't sure if the `#[derive(Serialize)]` would work given we use - /// `B::Comments`, which would borrow from the original source. #[test] - #[cfg(feature = "serde-1")] - fn you_can_actually_serialize_lines() { - let src = "G01 X5 G90 (comment) G91 M10\nG01\n"; - let line = parse(src).next().unwrap(); - - fn assert_serializable(_: &S) {} - fn assert_deserializable<'de, D: serde::Deserialize<'de>>() {} - - assert_serializable(&line); - assert_deserializable::>(); - } - - /// For some reason we were parsing the G90, then an empty G01 and the - /// actual G01. - #[test] - #[ignore] fn funny_bug_in_crate_example() { let src = "G90 \n G01 X50.0 Y-10"; let expected = vec![ @@ -433,7 +424,58 @@ mod tests { ]; let got: Vec<_> = crate::parse(src).collect(); + assert_eq!(got, expected); + } + + #[test] + fn implicit_command_after_newline() { + let src = "M3\nG01 X1.0 Y2.0\nX3.0 Y4.0"; + let expected = vec![ + GCode::new(Mnemonic::Miscellaneous, 3.0, Span::PLACEHOLDER), + GCode::new(Mnemonic::General, 1.0, Span::PLACEHOLDER) + .with_argument(Word::new('X', 1.0, Span::PLACEHOLDER)) + .with_argument(Word::new('Y', 2.0, Span::PLACEHOLDER)), + GCode::new(Mnemonic::General, 1.0, Span::PLACEHOLDER) + .with_argument(Word::new('X', 3.0, Span::PLACEHOLDER)) + .with_argument(Word::new('Y', 4.0, Span::PLACEHOLDER)), + ]; + + let got: Vec<_> = crate::parse(src).collect(); + assert_eq!(got, expected); + } + + #[test] + fn implicit_command_standalone() { + let src = "G01 X1.0 Y2.0\nX3.0 Y4.0"; + let expected = vec![ + GCode::new(Mnemonic::General, 1.0, Span::PLACEHOLDER) + .with_argument(Word::new('X', 1.0, Span::PLACEHOLDER)) + .with_argument(Word::new('Y', 2.0, Span::PLACEHOLDER)), + GCode::new(Mnemonic::General, 1.0, Span::PLACEHOLDER) + .with_argument(Word::new('X', 3.0, Span::PLACEHOLDER)) + .with_argument(Word::new('Y', 4.0, Span::PLACEHOLDER)), + ]; + let got: Vec<_> = crate::parse(src).collect(); + assert_eq!(got, expected); + } + + #[test] + // This test focuses on the G90 and M7 on the same line. + fn implicit_command_two_commands_on_line() { + let src = "G90 M7\nG01 X1.0 Y2.0\nX3.0 Y4.0"; + let expected = vec![ + GCode::new(Mnemonic::General, 90.0, Span::PLACEHOLDER), + GCode::new(Mnemonic::Miscellaneous, 7.0, Span::PLACEHOLDER), + GCode::new(Mnemonic::General, 1.0, Span::PLACEHOLDER) + .with_argument(Word::new('X', 1.0, Span::PLACEHOLDER)) + .with_argument(Word::new('Y', 2.0, Span::PLACEHOLDER)), + GCode::new(Mnemonic::General, 1.0, Span::PLACEHOLDER) + .with_argument(Word::new('X', 3.0, Span::PLACEHOLDER)) + .with_argument(Word::new('Y', 4.0, Span::PLACEHOLDER)), + ]; + + let got: Vec<_> = crate::parse(src).collect(); assert_eq!(got, expected); } } diff --git a/gcode/src/words.rs b/gcode/src/words.rs index d4ad8993..ef202b4d 100644 --- a/gcode/src/words.rs +++ b/gcode/src/words.rs @@ -42,22 +42,13 @@ impl Display for Word { pub(crate) enum Atom<'input> { Word(Word), Comment(Comment<'input>), + Newline(Token<'input>), /// Incomplete parts of a [`Word`]. BrokenWord(Token<'input>), /// Garbage from the tokenizer (see [`TokenType::Unknown`]). Unknown(Token<'input>), } -impl<'input> Atom<'input> { - pub(crate) fn span(&self) -> Span { - match self { - Atom::Word(word) => word.span, - Atom::Comment(comment) => comment.span, - Atom::Unknown(token) | Atom::BrokenWord(token) => token.span, - } - } -} - #[derive(Debug, Clone, PartialEq)] pub(crate) struct WordsOrComments<'input, I> { tokens: I, @@ -90,6 +81,7 @@ where match kind { TokenType::Unknown => return Some(Atom::Unknown(token)), + TokenType::Newline => return Some(Atom::Newline(token)), TokenType::Comment => { return Some(Atom::Comment(Comment { value, span })) }, diff --git a/gcode/tests/smoke_test.rs b/gcode/tests/smoke_test.rs index ad06b8cf..77733b86 100644 --- a/gcode/tests/smoke_test.rs +++ b/gcode/tests/smoke_test.rs @@ -1,4 +1,4 @@ -use gcode::{GCode, Mnemonic, Span, Word}; +use gcode::{Mnemonic, Span, Word}; macro_rules! smoke_test { ($name:ident, $filename:expr) => { @@ -26,25 +26,7 @@ smoke_test!(pi_rustlogo, "PI_rustlogo.gcode"); smoke_test!(insulpro_piping, "Insulpro.Piping.-.115mm.OD.-.40mm.WT.txt"); #[test] -#[ignore] fn expected_program_2_output() { - // N10 T2 M3 S447 F80 - // N20 G0 X112 Y-2 - // ;N30 Z-5 - // N40 G41 - // N50 G1 X95 Y8 M8 - // ;N60 X32 - // ;N70 X5 Y15 - // ;N80 Y52 - // N90 G2 X15 Y62 I10 J0 - // N100 G1 X83 - // N110 G3 X95 Y50 I12 J0 - // N120 G1 Y-12 - // N130 G40 - // N140 G0 Z100 M9 - // ;N150 X150 Y150 - // N160 M30 - let src = include_str!("data/program_2.gcode"); let got: Vec<_> = @@ -54,15 +36,6 @@ fn expected_program_2_output() { assert_eq!(got.len(), 20); // check lines without any comments assert_eq!(got.iter().filter(|l| l.comments().is_empty()).count(), 11); - - let gcodes: Vec<_> = got.iter().flat_map(|l| l.gcodes()).cloned().collect(); - let expected = vec![ - GCode::new(Mnemonic::ToolChange, 2.0, Span::PLACEHOLDER), - GCode::new(Mnemonic::Miscellaneous, 3.0, Span::PLACEHOLDER) - .with_argument(Word::new('S', 447.0, Span::PLACEHOLDER)) - .with_argument(Word::new('F', 80.0, Span::PLACEHOLDER)), - ]; - pretty_assertions::assert_eq!(gcodes, expected); } struct PanicOnError;