1#[cfg(test)]
6#[cfg(feature = "std")]
7mod test;
8mod token;
9
10#[cfg(feature = "alloc")]
11use alloc::vec::Vec;
12
13use winnow::stream::AsBStr as _;
14use winnow::stream::ContainsToken as _;
15use winnow::stream::FindSlice as _;
16use winnow::stream::Location;
17use winnow::stream::Stream as _;
18
19use crate::Span;
20
21pub use token::Token;
22pub use token::TokenKind;
23
24pub struct Lexer<'i> {
28 stream: Stream<'i>,
29 eof: bool,
30}
31
32impl<'i> Lexer<'i> {
33 pub(crate) fn new(input: &'i str) -> Self {
34 let mut stream = Stream::new(input);
35 if input.as_bytes().starts_with(BOM) {
36 let offset = BOM.len();
37 #[cfg(feature = "unsafe")] unsafe {
39 stream.next_slice_unchecked(offset)
40 };
41 #[cfg(not(feature = "unsafe"))]
42 stream.next_slice(offset);
43 }
44 Lexer { stream, eof: false }
45 }
46
47 #[cfg(feature = "alloc")]
48 pub fn into_vec(self) -> Vec<Token> {
49 let capacity = self.stream.len().div_ceil(4);
50 let mut vec = Vec::with_capacity(capacity);
51 vec.extend(self);
52 vec
53 }
54}
55
56impl Iterator for Lexer<'_> {
57 type Item = Token;
58
59 fn next(&mut self) -> Option<Self::Item> {
60 let Some(peek_byte) = self.stream.as_bstr().first() else {
61 if self.eof {
62 return None;
63 } else {
64 self.eof = true;
65 let start = self.stream.current_token_start();
66 let span = Span::new_unchecked(start, start);
67 return Some(Token::new(TokenKind::Eof, span));
68 }
69 };
70 Some(process_token(*peek_byte, &mut self.stream))
71 }
72}
73
74const BOM: &[u8] = b"\xEF\xBB\xBF";
75
76pub(crate) type Stream<'i> = winnow::stream::LocatingSlice<&'i str>;
77
78fn process_token(peek_byte: u8, stream: &mut Stream<'_>) -> Token {
79 let token = match peek_byte {
80 b'.' => lex_ascii_char(stream, TokenKind::Dot),
81 b'=' => lex_ascii_char(stream, TokenKind::Equals),
82 b',' => lex_ascii_char(stream, TokenKind::Comma),
83 b'[' => lex_ascii_char(stream, TokenKind::LeftSquareBracket),
84 b']' => lex_ascii_char(stream, TokenKind::RightSquareBracket),
85 b'{' => lex_ascii_char(stream, TokenKind::LeftCurlyBracket),
86 b'}' => lex_ascii_char(stream, TokenKind::RightCurlyBracket),
87 b' ' => lex_whitespace(stream),
88 b'\t' => lex_whitespace(stream),
89 b'#' => lex_comment(stream),
90 b'\r' => lex_crlf(stream),
91 b'\n' => lex_ascii_char(stream, TokenKind::Newline),
92 b'\'' => {
93 if stream.starts_with(ML_LITERAL_STRING_DELIM) {
94 lex_ml_literal_string(stream)
95 } else {
96 lex_literal_string(stream)
97 }
98 }
99 b'"' => {
100 if stream.starts_with(ML_BASIC_STRING_DELIM) {
101 lex_ml_basic_string(stream)
102 } else {
103 lex_basic_string(stream)
104 }
105 }
106 _ => lex_atom(stream),
107 };
108 token
109}
110
111fn lex_ascii_char(stream: &mut Stream<'_>, kind: TokenKind) -> Token {
119 debug_assert!(!stream.is_empty());
120 let start = stream.current_token_start();
121
122 let offset = 1; #[cfg(feature = "unsafe")] unsafe {
125 stream.next_slice_unchecked(offset)
126 };
127 #[cfg(not(feature = "unsafe"))]
128 stream.next_slice(offset);
129
130 let end = stream.previous_token_end();
131 let span = Span::new_unchecked(start, end);
132 Token::new(kind, span)
133}
134
135fn lex_whitespace(stream: &mut Stream<'_>) -> Token {
150 debug_assert!(!stream.is_empty());
151 let start = stream.current_token_start();
152
153 let offset = stream
154 .as_bstr()
155 .offset_for(|b| !WSCHAR.contains_token(b))
156 .unwrap_or(stream.eof_offset());
157 #[cfg(feature = "unsafe")] unsafe {
159 stream.next_slice_unchecked(offset)
160 };
161 #[cfg(not(feature = "unsafe"))]
162 stream.next_slice(offset);
163
164 let end = stream.previous_token_end();
165 let span = Span::new_unchecked(start, end);
166 Token::new(TokenKind::Whitespace, span)
167}
168
169pub(crate) const WSCHAR: (u8, u8) = (b' ', b'\t');
174
175fn lex_comment(stream: &mut Stream<'_>) -> Token {
192 let start = stream.current_token_start();
193
194 let offset = stream
195 .as_bytes()
196 .find_slice((b'\r', b'\n'))
197 .map(|s| s.start)
198 .unwrap_or_else(|| stream.eof_offset());
199 #[cfg(feature = "unsafe")] unsafe {
201 stream.next_slice_unchecked(offset)
202 };
203 #[cfg(not(feature = "unsafe"))]
204 stream.next_slice(offset);
205
206 let end = stream.previous_token_end();
207 let span = Span::new_unchecked(start, end);
208 Token::new(TokenKind::Comment, span)
209}
210
211pub(crate) const COMMENT_START_SYMBOL: u8 = b'#';
215
216fn lex_crlf(stream: &mut Stream<'_>) -> Token {
230 let start = stream.current_token_start();
231
232 let mut offset = '\r'.len_utf8();
233 let has_lf = stream.as_bstr().get(1) == Some(&b'\n');
234 if has_lf {
235 offset += '\n'.len_utf8();
236 }
237
238 #[cfg(feature = "unsafe")] unsafe {
240 stream.next_slice_unchecked(offset)
241 };
242 #[cfg(not(feature = "unsafe"))]
243 stream.next_slice(offset);
244 let end = stream.previous_token_end();
245 let span = Span::new_unchecked(start, end);
246
247 Token::new(TokenKind::Newline, span)
248}
249
250fn lex_literal_string(stream: &mut Stream<'_>) -> Token {
267 let start = stream.current_token_start();
268
269 let offset = 1; #[cfg(feature = "unsafe")] unsafe {
272 stream.next_slice_unchecked(offset)
273 };
274 #[cfg(not(feature = "unsafe"))]
275 stream.next_slice(offset);
276
277 let offset = match stream.as_bstr().find_slice((APOSTROPHE, b'\n')) {
278 Some(span) => {
279 if stream.as_bstr()[span.start] == APOSTROPHE {
280 span.end
281 } else {
282 span.start
283 }
284 }
285 None => stream.eof_offset(),
286 };
287 #[cfg(feature = "unsafe")]
288 unsafe {
290 stream.next_slice_unchecked(offset)
291 };
292 #[cfg(not(feature = "unsafe"))]
293 stream.next_slice(offset);
294
295 let end = stream.previous_token_end();
296 let span = Span::new_unchecked(start, end);
297 Token::new(TokenKind::LiteralString, span)
298}
299
300pub(crate) const APOSTROPHE: u8 = b'\'';
304
305fn lex_ml_literal_string(stream: &mut Stream<'_>) -> Token {
324 let start = stream.current_token_start();
325
326 let offset = ML_LITERAL_STRING_DELIM.len();
327 #[cfg(feature = "unsafe")] unsafe {
329 stream.next_slice_unchecked(offset)
330 };
331 #[cfg(not(feature = "unsafe"))]
332 stream.next_slice(offset);
333
334 let offset = match stream.as_bstr().find_slice(ML_LITERAL_STRING_DELIM) {
335 Some(span) => span.end,
336 None => stream.eof_offset(),
337 };
338 #[cfg(feature = "unsafe")]
339 unsafe {
341 stream.next_slice_unchecked(offset)
342 };
343 #[cfg(not(feature = "unsafe"))]
344 stream.next_slice(offset);
345
346 if stream.as_bstr().peek_token() == Some(APOSTROPHE) {
347 let offset = 1;
348 #[cfg(feature = "unsafe")] unsafe {
350 stream.next_slice_unchecked(offset)
351 };
352 #[cfg(not(feature = "unsafe"))]
353 stream.next_slice(offset);
354
355 if stream.as_bstr().peek_token() == Some(APOSTROPHE) {
356 let offset = 1;
357 #[cfg(feature = "unsafe")]
358 unsafe {
360 stream.next_slice_unchecked(offset)
361 };
362 #[cfg(not(feature = "unsafe"))]
363 stream.next_slice(offset);
364 }
365 }
366
367 let end = stream.previous_token_end();
368 let span = Span::new_unchecked(start, end);
369 Token::new(TokenKind::MlLiteralString, span)
370}
371
372pub(crate) const ML_LITERAL_STRING_DELIM: &str = "'''";
376
377fn lex_basic_string(stream: &mut Stream<'_>) -> Token {
409 let start = stream.current_token_start();
410
411 let offset = 1; #[cfg(feature = "unsafe")] unsafe {
414 stream.next_slice_unchecked(offset)
415 };
416 #[cfg(not(feature = "unsafe"))]
417 stream.next_slice(offset);
418
419 loop {
420 match stream.as_bstr().find_slice((QUOTATION_MARK, ESCAPE, b'\n')) {
422 Some(span) => {
423 let found = stream.as_bstr()[span.start];
424 if found == QUOTATION_MARK {
425 let offset = span.end;
426 #[cfg(feature = "unsafe")]
427 unsafe {
429 stream.next_slice_unchecked(offset)
430 };
431 #[cfg(not(feature = "unsafe"))]
432 stream.next_slice(offset);
433 break;
434 } else if found == ESCAPE {
435 let offset = span.end;
436 #[cfg(feature = "unsafe")]
437 unsafe {
439 stream.next_slice_unchecked(offset)
440 };
441 #[cfg(not(feature = "unsafe"))]
442 stream.next_slice(offset);
443
444 let peek = stream.as_bstr().peek_token();
445 match peek {
446 Some(ESCAPE) | Some(QUOTATION_MARK) => {
447 let offset = 1; #[cfg(feature = "unsafe")]
449 #[cfg(feature = "unsafe")]
450 unsafe {
452 stream.next_slice_unchecked(offset)
453 };
454 #[cfg(not(feature = "unsafe"))]
455 stream.next_slice(offset);
456 }
457 _ => {}
458 }
459 continue;
460 } else if found == b'\n' {
461 let offset = span.start;
462 #[cfg(feature = "unsafe")]
463 unsafe {
465 stream.next_slice_unchecked(offset)
466 };
467 #[cfg(not(feature = "unsafe"))]
468 stream.next_slice(offset);
469 break;
470 } else {
471 unreachable!("found `{found}`");
472 }
473 }
474 None => {
475 stream.finish();
476 break;
477 }
478 }
479 }
480
481 let end = stream.previous_token_end();
482 let span = Span::new_unchecked(start, end);
483 Token::new(TokenKind::BasicString, span)
484}
485
486pub(crate) const QUOTATION_MARK: u8 = b'"';
490
491pub(crate) const ESCAPE: u8 = b'\\';
495
496fn lex_ml_basic_string(stream: &mut Stream<'_>) -> Token {
516 let start = stream.current_token_start();
517
518 let offset = ML_BASIC_STRING_DELIM.len();
519 #[cfg(feature = "unsafe")] unsafe {
521 stream.next_slice_unchecked(offset)
522 };
523 #[cfg(not(feature = "unsafe"))]
524 stream.next_slice(offset);
525
526 loop {
527 match stream.as_bstr().find_slice((ML_BASIC_STRING_DELIM, "\\")) {
529 Some(span) => {
530 let found = stream.as_bstr()[span.start];
531 if found == QUOTATION_MARK {
532 let offset = span.end;
533 #[cfg(feature = "unsafe")]
534 unsafe {
536 stream.next_slice_unchecked(offset)
537 };
538 #[cfg(not(feature = "unsafe"))]
539 stream.next_slice(offset);
540 break;
541 } else if found == ESCAPE {
542 let offset = span.end;
543 #[cfg(feature = "unsafe")]
544 unsafe {
546 stream.next_slice_unchecked(offset)
547 };
548 #[cfg(not(feature = "unsafe"))]
549 stream.next_slice(offset);
550
551 let peek = stream.as_bstr().peek_token();
552 match peek {
553 Some(ESCAPE) | Some(QUOTATION_MARK) => {
554 let offset = 1; #[cfg(feature = "unsafe")]
556 unsafe {
558 stream.next_slice_unchecked(offset)
559 };
560 #[cfg(not(feature = "unsafe"))]
561 stream.next_slice(offset);
562 }
563 _ => {}
564 }
565 continue;
566 } else {
567 unreachable!("found `{found}`");
568 }
569 }
570 None => {
571 stream.finish();
572 break;
573 }
574 }
575 }
576 if stream.as_bstr().peek_token() == Some(QUOTATION_MARK) {
577 let offset = 1;
578 #[cfg(feature = "unsafe")]
579 unsafe {
581 stream.next_slice_unchecked(offset)
582 };
583 #[cfg(not(feature = "unsafe"))]
584 stream.next_slice(offset);
585 if stream.as_bstr().peek_token() == Some(QUOTATION_MARK) {
586 let offset = 1;
587 #[cfg(feature = "unsafe")]
588 unsafe {
590 stream.next_slice_unchecked(offset)
591 };
592 #[cfg(not(feature = "unsafe"))]
593 stream.next_slice(offset);
594 }
595 }
596
597 let end = stream.previous_token_end();
598 let span = Span::new_unchecked(start, end);
599 Token::new(TokenKind::MlBasicString, span)
600}
601
602pub(crate) const ML_BASIC_STRING_DELIM: &str = "\"\"\"";
606
607fn lex_atom(stream: &mut Stream<'_>) -> Token {
616 let start = stream.current_token_start();
617
618 const TOKEN_START: &[u8] = b".=,[]{} \t#\r\n";
620 let offset = stream
621 .as_bstr()
622 .offset_for(|b| TOKEN_START.contains_token(b))
623 .unwrap_or_else(|| stream.eof_offset());
624 #[cfg(feature = "unsafe")] unsafe {
626 stream.next_slice_unchecked(offset)
627 };
628 #[cfg(not(feature = "unsafe"))]
629 stream.next_slice(offset);
630
631 let end = stream.previous_token_end();
632 let span = Span::new_unchecked(start, end);
633 Token::new(TokenKind::Atom, span)
634}