1use memchr::{memchr, memchr_iter, memrchr};
14use molframe_core::span::{ByteSpan, Position};
15
16#[derive(Clone, Copy, PartialEq, Eq, Debug)]
18pub enum Quoting {
19 Bare,
21 Single,
23 Double,
25 Text,
27}
28
29#[derive(Clone, Copy, PartialEq, Eq, Debug)]
31pub enum Token<'a> {
32 Block(&'a str),
34 FrameStart(&'a str),
36 FrameEnd,
38 Loop,
40 Tag(&'a str),
42 Value(&'a str, Quoting),
44}
45
46#[derive(Clone, Copy, PartialEq, Eq, Debug)]
48pub struct Spanned<'a> {
49 pub token: Token<'a>,
51 pub span: ByteSpan,
53}
54
55#[derive(Clone, Copy, PartialEq, Eq, Debug)]
57pub enum LexError {
58 UnterminatedQuote(Position),
60 UnterminatedText(Position),
62 NotText,
64 PositionOverflow(Position),
66}
67
68#[derive(Clone, Debug)]
81pub struct Lexer<'a> {
82 input: &'a str,
83 at: Position,
84}
85
86impl<'a> Lexer<'a> {
87 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 #[must_use]
102 pub const fn position(&self) -> Position {
103 self.at
104 }
105
106 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 fn keyword_or_value(&mut self) -> Result<Token<'a>, LexError> {
133 let word = self.take_bare()?;
134
135 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 fn advance(&mut self, count: usize) -> Result<(), LexError> {
180 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 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 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 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 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 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 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 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 fn take_text(&mut self) -> Result<&'a str, LexError> {
349 let opened_at = self.at;
350 self.advance(1)?;
351 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
371fn slice_from(text: &str, start: usize) -> &str {
373 let Some(slice) = text.get(start..) else {
374 return "";
375 };
376 slice
377}
378
379fn slice_to(text: &str, end: usize) -> &str {
381 let Some(slice) = text.get(..end) else {
382 return "";
383 };
384 slice
385}
386
387fn 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;