Skip to main content

molframe_cif/lexer/
tokenize.rs

1//! Turning bytes into tokens.
2//!
3//! Hand-written over `&[u8]` rather than assembled from combinators, for one
4//! reason that matters more than speed: the cursor already knows its offset,
5//! line and column, so every token carries an exact location for free. A parser
6//! built on top of it can point at the character that went wrong instead of
7//! naming the file.
8//!
9//! Nothing here allocates. A token borrows its text from the input, and the
10//! quoting style travels alongside so that a later stage can tell a value that
11//! was written `.` from one that was written `'.'`.
12
13use memchr::{memchr, memchr_iter, memrchr};
14use molframe_core::span::{ByteSpan, Position};
15
16/// How a value was written, which decides what it means.
17#[derive(Clone, Copy, PartialEq, Eq, Debug)]
18pub enum Quoting {
19    /// Written without quotes.
20    Bare,
21    /// Written between single quotes.
22    Single,
23    /// Written between double quotes.
24    Double,
25    /// Written as a semicolon-delimited block.
26    Text,
27}
28
29/// What the lexer found.
30#[derive(Clone, Copy, PartialEq, Eq, Debug)]
31pub enum Token<'a> {
32    /// The start of a data block, carrying its name.
33    Block(&'a str),
34    /// The start of a save frame, carrying its name.
35    FrameStart(&'a str),
36    /// The end of a save frame.
37    FrameEnd,
38    /// A loop header.
39    Loop,
40    /// An item name, including its leading underscore.
41    Tag(&'a str),
42    /// A value, with how it was written.
43    Value(&'a str, Quoting),
44}
45
46/// A token and where it came from.
47#[derive(Clone, Copy, PartialEq, Eq, Debug)]
48pub struct Spanned<'a> {
49    /// What was found.
50    pub token: Token<'a>,
51    /// Where it was found.
52    pub span: ByteSpan,
53}
54
55/// Something the lexer could not get past.
56#[derive(Clone, Copy, PartialEq, Eq, Debug)]
57pub enum LexError {
58    /// A quoted value ran to the end of its line without closing.
59    UnterminatedQuote(Position),
60    /// A semicolon-delimited block ran to the end of the file without closing.
61    UnterminatedText(Position),
62    /// The input is not valid text.
63    NotText,
64    /// A source location exceeded the core's 32-bit position representation.
65    PositionOverflow(Position),
66}
67
68/// Walks the tokens of a file.
69///
70/// # Examples
71///
72/// ```
73/// use molframe_cif::lexer::{Lexer, Token};
74///
75/// let mut lexer = Lexer::new(b"data_test\n_entry.id 1ABC\n")?;
76/// let first = lexer.next_token()?;
77/// assert_eq!(first.map(|spanned| spanned.token), Some(Token::Block("test")));
78/// # Ok::<(), molframe_cif::lexer::LexError>(())
79/// ```
80#[derive(Clone, Debug)]
81pub struct Lexer<'a> {
82    input: &'a str,
83    at: Position,
84}
85
86impl<'a> Lexer<'a> {
87    /// Creates a lexer over `bytes`.
88    ///
89    /// # Errors
90    ///
91    /// Returns [`LexError::NotText`] when the bytes are not valid text.
92    pub fn new(bytes: &'a [u8]) -> Result<Self, LexError> {
93        let input = str::from_utf8(bytes).map_err(|_| LexError::NotText)?;
94        Ok(Self {
95            input,
96            at: Position::START,
97        })
98    }
99
100    /// Where the lexer has reached.
101    #[must_use]
102    pub const fn position(&self) -> Position {
103        self.at
104    }
105
106    /// Reads the next token, or `None` at the end of the input.
107    ///
108    /// # Errors
109    ///
110    /// Returns the reason the input could not be got past.
111    pub fn next_token(&mut self) -> Result<Option<Spanned<'a>>, LexError> {
112        self.skip_trivia()?;
113        let start = self.at;
114        let Some(byte) = self.peek() else {
115            return Ok(None);
116        };
117
118        let token = match byte {
119            b'_' => Token::Tag(self.take_bare()?),
120            b'\'' => Token::Value(self.take_quoted(b'\'')?, Quoting::Single),
121            b'"' => Token::Value(self.take_quoted(b'"')?, Quoting::Double),
122            b';' if start.column == 1 => Token::Value(self.take_text()?, Quoting::Text),
123            _ => self.keyword_or_value()?,
124        };
125        Ok(Some(Spanned {
126            token,
127            span: ByteSpan::new(start, self.at.byte_offset),
128        }))
129    }
130
131    /// A bare word, which may be a keyword or an ordinary value.
132    fn keyword_or_value(&mut self) -> Result<Token<'a>, LexError> {
133        let word = self.take_bare()?;
134
135        // Nearly every bare word in a coordinate loop is a value, and the three
136        // keywords begin with three distinct letters. Deciding on the first
137        // byte turns three prefix comparisons per value into one.
138        match word.as_bytes().first() {
139            Some(b'd' | b'D') => {
140                if let Some(name) = strip_prefix_ignore_case(word, "data_") {
141                    return Ok(Token::Block(name));
142                }
143            }
144            Some(b's' | b'S') => {
145                if let Some(name) = strip_prefix_ignore_case(word, "save_") {
146                    return Ok(if name.is_empty() {
147                        Token::FrameEnd
148                    } else {
149                        Token::FrameStart(name)
150                    });
151                }
152            }
153            Some(b'l' | b'L') if word.eq_ignore_ascii_case("loop_") => {
154                return Ok(Token::Loop);
155            }
156            _ => {}
157        }
158        Ok(Token::Value(word, Quoting::Bare))
159    }
160
161    fn peek(&self) -> Option<u8> {
162        self.remaining().as_bytes().first().copied()
163    }
164
165    fn remaining(&self) -> &'a str {
166        match usize::try_from(self.at.byte_offset) {
167            Ok(offset) => slice_from(self.input, offset),
168            Err(_) => "",
169        }
170    }
171
172    /// Advances over `count` bytes, keeping the line and column right.
173    ///
174    /// A long run has its newlines found with a vectorised search; a short one
175    /// walks its bytes. The threshold is not a micro-optimisation but the whole
176    /// point: most advances in an `atom_site` loop are a single separating
177    /// space, and setting up a vectorised search over one byte costs more than
178    /// inspecting it.
179    fn advance(&mut self, count: usize) -> Result<(), LexError> {
180        /// Shortest run for which a vectorised newline search pays for itself.
181        const BULK_THRESHOLD: usize = 32;
182
183        let text = self.remaining();
184        let skipped = match text.as_bytes().get(..count) {
185            Some(skipped) => skipped,
186            None => text.as_bytes(),
187        };
188        if skipped.len() < BULK_THRESHOLD {
189            return self.advance_bytewise(skipped);
190        }
191        self.advance_all(skipped)
192    }
193
194    /// Advances over a short run one byte at a time.
195    fn advance_bytewise(&mut self, skipped: &[u8]) -> Result<(), LexError> {
196        for byte in skipped {
197            self.at = self
198                .at
199                .advance(*byte)
200                .ok_or(LexError::PositionOverflow(self.at))?;
201        }
202        Ok(())
203    }
204
205    /// Advances over exactly `skipped`, updating the position in bulk.
206    fn advance_all(&mut self, skipped: &[u8]) -> Result<(), LexError> {
207        let Ok(length) = u64::try_from(skipped.len()) else {
208            return Err(LexError::PositionOverflow(self.at));
209        };
210        let Some(byte_offset) = self.at.byte_offset.checked_add(length) else {
211            return Err(LexError::PositionOverflow(self.at));
212        };
213
214        let newlines = memchr_iter(b'\n', skipped).count();
215        if newlines == 0 {
216            let Ok(narrow) = u64::try_from(skipped.len()) else {
217                return Err(LexError::PositionOverflow(self.at));
218            };
219            let Some(column) = self.at.column.checked_add(narrow) else {
220                return Err(LexError::PositionOverflow(self.at));
221            };
222            self.at.byte_offset = byte_offset;
223            self.at.column = column;
224            return Ok(());
225        }
226
227        let Ok(newlines) = u64::try_from(newlines) else {
228            return Err(LexError::PositionOverflow(self.at));
229        };
230        let Some(line) = self.at.line.checked_add(newlines) else {
231            return Err(LexError::PositionOverflow(self.at));
232        };
233        let Some(last) = memrchr(b'\n', skipped) else {
234            return Err(LexError::PositionOverflow(self.at));
235        };
236        let Ok(after) = u64::try_from(skipped.len() - last - 1) else {
237            return Err(LexError::PositionOverflow(self.at));
238        };
239        let Some(column) = after.checked_add(1) else {
240            return Err(LexError::PositionOverflow(self.at));
241        };
242
243        self.at.byte_offset = byte_offset;
244        self.at.line = line;
245        self.at.column = column;
246        Ok(())
247    }
248
249    /// Advances a run the caller has already established holds no newline.
250    ///
251    /// Bare values are scanned once to find their end, and that scan proves the
252    /// absence of a newline. Re-deriving it here would walk the same bytes a
253    /// second time, so this is pure arithmetic — which matters because in an
254    /// `atom_site` loop nearly every token takes this path.
255    fn advance_non_newline(&mut self, count: usize) -> Result<(), LexError> {
256        let Ok(narrow) = u64::try_from(count) else {
257            return self.advance(count);
258        };
259        let Some(byte_offset) = self.at.byte_offset.checked_add(narrow) else {
260            return self.advance(count);
261        };
262        let Some(column) = self.at.column.checked_add(narrow) else {
263            return self.advance(count);
264        };
265        self.at.byte_offset = byte_offset;
266        self.at.column = column;
267        Ok(())
268    }
269
270    /// Skips whitespace and comments.
271    ///
272    /// A comment runs to the end of its line, and a `#` inside a quoted value is
273    /// an ordinary character — which is why this only runs between tokens.
274    fn skip_trivia(&mut self) -> Result<(), LexError> {
275        loop {
276            let text = self.remaining();
277            let bytes = text.as_bytes();
278            let spaces = match bytes.iter().position(|byte| !byte.is_ascii_whitespace()) {
279                Some(spaces) => spaces,
280                None => bytes.len(),
281            };
282            // Advance over the run already located, rather than counting it
283            // here and letting `advance` walk the same bytes a second time.
284            match bytes.get(..spaces) {
285                Some(run) => self.advance_all(run)?,
286                None => self.advance(spaces)?,
287            }
288
289            if self.peek() != Some(b'#') {
290                return Ok(());
291            }
292            let rest = self.remaining();
293            let line = match memchr(b'\n', rest.as_bytes()) {
294                Some(line) => line,
295                None => rest.len(),
296            };
297            self.advance(line)?;
298        }
299    }
300
301    /// A run of non-whitespace.
302    fn take_bare(&mut self) -> Result<&'a str, LexError> {
303        let text = self.remaining();
304        let end = text
305            .bytes()
306            .take_while(|byte| !byte.is_ascii_whitespace())
307            .count();
308        self.advance_non_newline(end)?;
309        Ok(slice_to(text, end))
310    }
311
312    /// A value between quotes.
313    ///
314    /// The closing quote is the one followed by whitespace or the end of the
315    /// line, so an apostrophe inside a quoted word does not end it early.
316    fn take_quoted(&mut self, quote: u8) -> Result<&'a str, LexError> {
317        let opened_at = self.at;
318        self.advance(1)?;
319        let text = self.remaining();
320        let bytes = text.as_bytes();
321
322        let mut cursor = 0usize;
323        while cursor < bytes.len() {
324            let Some(offset) = memchr(quote, bytes_from(bytes, cursor)) else {
325                break;
326            };
327            let candidate = cursor + offset;
328            let closes = match bytes.get(candidate + 1) {
329                None => true,
330                Some(next) => next.is_ascii_whitespace(),
331            };
332            if closes {
333                self.advance(candidate + 1)?;
334                return Ok(slice_to(text, candidate));
335            }
336            if bytes.get(candidate).is_some_and(|byte| *byte == b'\n') {
337                break;
338            }
339            cursor = candidate + 1;
340        }
341        Err(LexError::UnterminatedQuote(opened_at))
342    }
343
344    /// A semicolon-delimited block.
345    ///
346    /// It begins with a semicolon in the first column and ends with the next
347    /// one, so a semicolon anywhere else is ordinary text.
348    fn take_text(&mut self) -> Result<&'a str, LexError> {
349        let opened_at = self.at;
350        self.advance(1)?;
351        // The rest of the opening line belongs to the value.
352        let text = self.remaining();
353        let bytes = text.as_bytes();
354
355        let mut cursor = 0usize;
356        loop {
357            let Some(offset) = memchr(b'\n', bytes_from(bytes, cursor)) else {
358                return Err(LexError::UnterminatedText(opened_at));
359            };
360            let line_start = cursor + offset + 1;
361            if bytes.get(line_start) == Some(&b';') {
362                let value = slice_to(text, cursor + offset);
363                self.advance(line_start + 1)?;
364                return Ok(value.trim_start_matches(['\r', '\n']));
365            }
366            cursor = line_start;
367        }
368    }
369}
370
371/// The text from `start` onwards, or nothing when `start` is past the end.
372fn slice_from(text: &str, start: usize) -> &str {
373    let Some(slice) = text.get(start..) else {
374        return "";
375    };
376    slice
377}
378
379/// The text up to `end`, or nothing when `end` does not fall on a boundary.
380fn slice_to(text: &str, end: usize) -> &str {
381    let Some(slice) = text.get(..end) else {
382        return "";
383    };
384    slice
385}
386
387/// The bytes from `start` onwards, or nothing when `start` is past the end.
388fn bytes_from(bytes: &[u8], start: usize) -> &[u8] {
389    match bytes.get(start..) {
390        Some(slice) => slice,
391        None => &[],
392    }
393}
394
395fn strip_prefix_ignore_case<'a>(text: &'a str, prefix: &str) -> Option<&'a str> {
396    let head = text.get(..prefix.len())?;
397    head.eq_ignore_ascii_case(prefix)
398        .then(|| text.get(prefix.len()..))?
399}
400
401#[cfg(test)]
402#[path = "tokenize_tests.rs"]
403mod tests;