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 offset: usize,
85 line: u64,
87 line_start: usize,
89}
90
91impl<'a> Lexer<'a> {
92 pub fn new(bytes: &'a [u8]) -> Result<Self, LexError> {
100 let input = str::from_utf8(bytes).map_err(|_| LexError::NotText)?;
101 if u64::try_from(input.len()).is_err() {
104 return Err(LexError::PositionOverflow(Position::START));
105 }
106 Ok(Self {
107 input,
108 offset: 0,
109 line: 1,
110 line_start: 0,
111 })
112 }
113
114 #[must_use]
116 pub fn position(&self) -> Position {
117 let column = (self.offset - self.line_start) as u64 + 1;
119 Position::new(self.offset as u64, self.line, column)
120 }
121
122 pub fn next_token(&mut self) -> Result<Option<Spanned<'a>>, LexError> {
128 self.skip_trivia();
129 let start = self.position();
130 let Some(byte) = self.peek() else {
131 return Ok(None);
132 };
133
134 let token = match byte {
135 b'_' => Token::Tag(self.take_bare()),
136 b'\'' => Token::Value(self.take_quoted(b'\'')?, Quoting::Single),
137 b'"' => Token::Value(self.take_quoted(b'"')?, Quoting::Double),
138 b';' if self.offset == self.line_start => {
139 Token::Value(self.take_text()?, Quoting::Text)
140 }
141 _ => self.keyword_or_value(),
142 };
143 Ok(Some(Spanned {
144 token,
145 span: ByteSpan::new(start, self.offset as u64),
146 }))
147 }
148
149 fn keyword_or_value(&mut self) -> Token<'a> {
151 let word = self.take_bare();
152
153 match word.as_bytes().first() {
157 Some(b'd' | b'D') => {
158 if let Some(name) = strip_prefix_ignore_case(word, "data_") {
159 return Token::Block(name);
160 }
161 }
162 Some(b's' | b'S') => {
163 if let Some(name) = strip_prefix_ignore_case(word, "save_") {
164 return if name.is_empty() {
165 Token::FrameEnd
166 } else {
167 Token::FrameStart(name)
168 };
169 }
170 }
171 Some(b'l' | b'L') if word.eq_ignore_ascii_case("loop_") => {
172 return Token::Loop;
173 }
174 _ => {}
175 }
176 Token::Value(word, Quoting::Bare)
177 }
178
179 fn peek(&self) -> Option<u8> {
180 self.input.as_bytes().get(self.offset).copied()
181 }
182
183 fn remaining(&self) -> &'a str {
184 slice_from(self.input, self.offset)
185 }
186
187 fn advance(&mut self, count: usize) {
194 const BULK_THRESHOLD: usize = 32;
196
197 let bytes = self.input.as_bytes();
198 let end = self.offset.saturating_add(count).min(bytes.len());
199 let Some(skipped) = bytes.get(self.offset..end) else {
200 return;
201 };
202 if skipped.len() < BULK_THRESHOLD {
203 for (index, byte) in skipped.iter().enumerate() {
204 if *byte == b'\n' {
205 self.line += 1;
206 self.line_start = self.offset + index + 1;
207 }
208 }
209 } else {
210 let newlines = memchr_iter(b'\n', skipped).count();
211 if let Some(last) = memrchr(b'\n', skipped) {
212 self.line += newlines as u64;
213 self.line_start = self.offset + last + 1;
214 }
215 }
216 self.offset = end;
217 }
218
219 fn advance_non_newline(&mut self, count: usize) {
223 self.offset += count;
224 }
225
226 fn skip_trivia(&mut self) {
231 loop {
232 let bytes = self.remaining().as_bytes();
233 let spaces = found_or(
234 bytes.iter().position(|byte| !byte.is_ascii_whitespace()),
235 bytes.len(),
236 );
237 self.advance(spaces);
238
239 if self.peek() != Some(b'#') {
240 return;
241 }
242 let rest = self.remaining();
243 let line = found_or(memchr(b'\n', rest.as_bytes()), rest.len());
244 self.advance(line);
245 }
246 }
247
248 fn take_bare(&mut self) -> &'a str {
250 let text = self.remaining();
251 let end = found_or(
252 text.bytes().position(|byte| byte.is_ascii_whitespace()),
253 text.len(),
254 );
255 self.advance_non_newline(end);
256 slice_to(text, end)
257 }
258
259 fn take_quoted(&mut self, quote: u8) -> Result<&'a str, LexError> {
264 let opened_at = self.position();
265 self.advance(1);
266 let text = self.remaining();
267 let bytes = text.as_bytes();
268
269 let mut cursor = 0usize;
270 while cursor < bytes.len() {
271 let Some(offset) = memchr(quote, bytes_from(bytes, cursor)) else {
272 break;
273 };
274 let candidate = cursor + offset;
275 let closes = match bytes.get(candidate + 1) {
276 None => true,
277 Some(next) => next.is_ascii_whitespace(),
278 };
279 if closes {
280 self.advance(candidate + 1);
281 return Ok(slice_to(text, candidate));
282 }
283 if bytes.get(candidate).is_some_and(|byte| *byte == b'\n') {
284 break;
285 }
286 cursor = candidate + 1;
287 }
288 Err(LexError::UnterminatedQuote(opened_at))
289 }
290
291 fn take_text(&mut self) -> Result<&'a str, LexError> {
296 let opened_at = self.position();
297 self.advance(1);
298 let text = self.remaining();
300 let bytes = text.as_bytes();
301
302 let mut cursor = 0usize;
303 loop {
304 let Some(offset) = memchr(b'\n', bytes_from(bytes, cursor)) else {
305 return Err(LexError::UnterminatedText(opened_at));
306 };
307 let line_start = cursor + offset + 1;
308 if bytes.get(line_start) == Some(&b';') {
309 let value = slice_to(text, cursor + offset);
310 self.advance(line_start + 1);
311 return Ok(value.trim_start_matches(['\r', '\n']));
312 }
313 cursor = line_start;
314 }
315 }
316}
317
318fn slice_from(text: &str, start: usize) -> &str {
320 let Some(slice) = text.get(start..) else {
321 return "";
322 };
323 slice
324}
325
326fn slice_to(text: &str, end: usize) -> &str {
328 let Some(slice) = text.get(..end) else {
329 return "";
330 };
331 slice
332}
333
334const fn found_or(found: Option<usize>, len: usize) -> usize {
336 match found {
337 Some(position) => position,
338 None => len,
339 }
340}
341
342fn bytes_from(bytes: &[u8], start: usize) -> &[u8] {
344 match bytes.get(start..) {
345 Some(slice) => slice,
346 None => &[],
347 }
348}
349
350fn strip_prefix_ignore_case<'a>(text: &'a str, prefix: &str) -> Option<&'a str> {
351 let head = text.get(..prefix.len())?;
352 head.eq_ignore_ascii_case(prefix)
353 .then(|| text.get(prefix.len()..))?
354}
355
356#[cfg(test)]
357#[path = "tokenize_tests.rs"]
358mod tests;