gcode/
words.rs

1use crate::{
2    lexer::{Lexer, Token, TokenType},
3    Comment, Span,
4};
5use core::fmt::{self, Display, Formatter};
6
7/// A [`char`]-[`f32`] pair, used for things like arguments (`X3.14`), command
8/// numbers (`G90`) and line numbers (`N10`).
9#[derive(Debug, Copy, Clone, PartialEq)]
10#[cfg_attr(
11    feature = "serde-1",
12    derive(serde_derive::Serialize, serde_derive::Deserialize)
13)]
14#[repr(C)]
15pub struct Word {
16    /// The letter part of this [`Word`].
17    pub letter: char,
18    /// The value part.
19    pub value: f32,
20    /// Where the [`Word`] lies in the original string.
21    pub span: Span,
22}
23
24impl Word {
25    /// Create a new [`Word`].
26    pub fn new(letter: char, value: f32, span: Span) -> Self {
27        Word {
28            letter,
29            value,
30            span,
31        }
32    }
33}
34
35impl Display for Word {
36    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
37        write!(f, "{}{}", self.letter, self.value)
38    }
39}
40
41#[derive(Debug, Copy, Clone, PartialEq)]
42pub(crate) enum Atom<'input> {
43    Word(Word),
44    Comment(Comment<'input>),
45    /// Incomplete parts of a [`Word`].
46    BrokenWord(Token<'input>),
47    /// Garbage from the tokenizer (see [`TokenType::Unknown`]).
48    Unknown(Token<'input>),
49}
50
51impl<'input> Atom<'input> {
52    pub(crate) fn span(&self) -> Span {
53        match self {
54            Atom::Word(word) => word.span,
55            Atom::Comment(comment) => comment.span,
56            Atom::Unknown(token) | Atom::BrokenWord(token) => token.span,
57        }
58    }
59}
60
61#[derive(Debug, Clone, PartialEq)]
62pub(crate) struct WordsOrComments<'input, I> {
63    tokens: I,
64    /// keep track of the last letter so we can deal with a trailing letter
65    /// that has no number
66    last_letter: Option<Token<'input>>,
67}
68
69impl<'input, I> WordsOrComments<'input, I>
70where
71    I: Iterator<Item = Token<'input>>,
72{
73    pub(crate) fn new(tokens: I) -> Self {
74        WordsOrComments {
75            tokens,
76            last_letter: None,
77        }
78    }
79}
80
81impl<'input, I> Iterator for WordsOrComments<'input, I>
82where
83    I: Iterator<Item = Token<'input>>,
84{
85    type Item = Atom<'input>;
86
87    fn next(&mut self) -> Option<Self::Item> {
88        while let Some(token) = self.tokens.next() {
89            let Token { kind, value, span } = token;
90
91            match kind {
92                TokenType::Unknown => return Some(Atom::Unknown(token)),
93                TokenType::Comment => {
94                    return Some(Atom::Comment(Comment { value, span }))
95                },
96                TokenType::Letter if self.last_letter.is_none() => {
97                    self.last_letter = Some(token);
98                },
99                TokenType::Number if self.last_letter.is_some() => {
100                    let letter_token = self.last_letter.take().unwrap();
101                    let span = letter_token.span.merge(span);
102
103                    debug_assert_eq!(letter_token.value.len(), 1);
104                    let letter = letter_token.value.chars().next().unwrap();
105                    let value = value.parse().expect("");
106
107                    return Some(Atom::Word(Word {
108                        letter,
109                        value,
110                        span,
111                    }));
112                },
113                _ => return Some(Atom::BrokenWord(token)),
114            }
115        }
116
117        self.last_letter.take().map(Atom::BrokenWord)
118    }
119}
120
121impl<'input> From<&'input str> for WordsOrComments<'input, Lexer<'input>> {
122    fn from(other: &'input str) -> WordsOrComments<'input, Lexer<'input>> {
123        WordsOrComments::new(Lexer::new(other))
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use crate::lexer::Lexer;
131
132    #[test]
133    fn pass_comments_through() {
134        let mut words =
135            WordsOrComments::new(Lexer::new("(this is a comment) 3.14"));
136
137        let got = words.next().unwrap();
138
139        let comment = "(this is a comment)";
140        let expected = Atom::Comment(Comment {
141            value: comment,
142            span: Span {
143                start: 0,
144                end: comment.len(),
145                line: 0,
146            },
147        });
148        assert_eq!(got, expected);
149    }
150
151    #[test]
152    fn pass_garbage_through() {
153        let text = "!@#$ *";
154        let mut words = WordsOrComments::new(Lexer::new(text));
155
156        let got = words.next().unwrap();
157
158        let expected = Atom::Unknown(Token {
159            value: text,
160            kind: TokenType::Unknown,
161            span: Span {
162                start: 0,
163                end: text.len(),
164                line: 0,
165            },
166        });
167        assert_eq!(got, expected);
168    }
169
170    #[test]
171    fn numbers_are_garbage_if_they_dont_have_a_letter_in_front() {
172        let text = "3.14 ()";
173        let mut words = WordsOrComments::new(Lexer::new(text));
174
175        let got = words.next().unwrap();
176
177        let expected = Atom::BrokenWord(Token {
178            value: "3.14",
179            kind: TokenType::Number,
180            span: Span {
181                start: 0,
182                end: 4,
183                line: 0,
184            },
185        });
186        assert_eq!(got, expected);
187    }
188
189    #[test]
190    fn recognise_a_valid_word() {
191        let text = "G90";
192        let mut words = WordsOrComments::new(Lexer::new(text));
193
194        let got = words.next().unwrap();
195
196        let expected = Atom::Word(Word {
197            letter: 'G',
198            value: 90.0,
199            span: Span {
200                start: 0,
201                end: text.len(),
202                line: 0,
203            },
204        });
205        assert_eq!(got, expected);
206    }
207}