1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct Pos {
11 pub line: usize,
12 pub column: usize,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16#[non_exhaustive]
17pub enum TokenKind {
18 LowerId {
20 qualifier: Option<String>,
21 name: String,
22 },
23 UpperId {
25 qualifier: Option<String>,
26 name: String,
27 },
28 Op(String),
30 IntLit(String),
31 DecimalLit(String),
32 StringLit(String),
33 CharLit(String),
34 LParen,
35 RParen,
36 LBracket,
37 RBracket,
38 LBrace,
39 RBrace,
40 Comma,
41 Semi,
42 Backtick,
43 VLBrace,
45 VRBrace,
47 VSemi,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct Token {
53 pub(crate) kind: TokenKind,
54 pub(crate) pos: Pos,
55 pub(crate) start: usize,
58 pub(crate) end: usize,
60}
61
62impl Token {
63 pub const fn kind(&self) -> &TokenKind {
64 &self.kind
65 }
66
67 pub const fn pos(&self) -> Pos {
68 self.pos
69 }
70
71 pub const fn start(&self) -> usize {
72 self.start
73 }
74
75 pub const fn end(&self) -> usize {
76 self.end
77 }
78
79 pub const fn is_virtual(&self) -> bool {
82 matches!(
83 self.kind,
84 TokenKind::VLBrace | TokenKind::VRBrace | TokenKind::VSemi
85 )
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
94#[non_exhaustive]
95pub enum TriviaKind {
96 LineComment,
98 BlockComment,
100 CppDirective,
102 BlankLines(usize),
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct Trivia {
108 pub(crate) kind: TriviaKind,
109 pub(crate) text: String,
111 pub(crate) pos: Pos,
112 pub(crate) start: usize,
113 pub(crate) end: usize,
114}
115
116impl Trivia {
117 pub const fn kind(&self) -> &TriviaKind {
118 &self.kind
119 }
120
121 pub fn text(&self) -> &str {
122 &self.text
123 }
124
125 pub const fn pos(&self) -> Pos {
126 self.pos
127 }
128
129 pub const fn start(&self) -> usize {
130 self.start
131 }
132
133 pub const fn end(&self) -> usize {
134 self.end
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct LexError {
142 pub kind: LexErrorKind,
143 pub pos: Pos,
144}
145
146impl std::fmt::Display for LexError {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 self.kind.fmt(f)
149 }
150}
151
152impl std::error::Error for LexError {}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
155#[non_exhaustive]
156pub enum LexErrorKind {
157 UnexpectedCharacter(char),
158 UnterminatedBlockComment,
159 UnterminatedStringLiteral,
160 UnterminatedStringGap,
161 InvalidEscapeSequence(char),
162 StraySingleQuote,
163 CharacterLiteralWrongLength,
164 HexLiteralMissingDigits,
165 DecimalExponentMissingDigits,
166}
167
168impl std::fmt::Display for LexErrorKind {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 match self {
171 Self::UnexpectedCharacter(c) => write!(f, "unexpected character '{c}'"),
172 Self::UnterminatedBlockComment => f.write_str("unterminated block comment"),
173 Self::UnterminatedStringLiteral => f.write_str("unterminated string literal"),
174 Self::UnterminatedStringGap => f.write_str("unterminated string gap"),
175 Self::InvalidEscapeSequence(c) => write!(f, "invalid escape sequence \\{c}"),
176 Self::StraySingleQuote => f.write_str("stray single quote"),
177 Self::CharacterLiteralWrongLength => {
178 f.write_str("character literal must contain exactly one character")
179 }
180 Self::HexLiteralMissingDigits => f.write_str("hex literal requires at least one digit"),
181 Self::DecimalExponentMissingDigits => {
182 f.write_str("decimal exponent requires at least one digit")
183 }
184 }
185 }
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct LexOutput {
190 pub tokens: Vec<Token>,
191 pub errors: Vec<LexError>,
192}
193
194impl LexOutput {
195 pub fn into_parts(self) -> (Vec<Token>, Vec<LexError>) {
196 (self.tokens, self.errors)
197 }
198}
199
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct LexWithTriviaOutput {
202 pub tokens: Vec<Token>,
203 pub trivia: Vec<Trivia>,
204 pub errors: Vec<LexError>,
205}
206
207impl LexWithTriviaOutput {
208 pub fn into_parts(self) -> (Vec<Token>, Vec<Trivia>, Vec<LexError>) {
209 (self.tokens, self.trivia, self.errors)
210 }
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
214#[non_exhaustive]
215pub enum RenderLosslessError {
216 OverlappingSpans {
217 start: usize,
218 },
219 UncoveredBytes {
220 start: usize,
221 end: usize,
222 text: String,
223 },
224 UncoveredTail {
225 start: usize,
226 text: String,
227 },
228}
229
230impl std::fmt::Display for RenderLosslessError {
231 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232 match self {
233 Self::OverlappingSpans { start } => write!(f, "overlapping spans at byte {start}"),
234 Self::UncoveredBytes { start, end, text } => write!(
235 f,
236 "bytes {start}..{end} lost (not covered by any token/trivia): {text:?}"
237 ),
238 Self::UncoveredTail { start, text } => {
239 write!(f, "bytes {start}.. lost at EOF: {text:?}")
240 }
241 }
242 }
243}
244
245impl std::error::Error for RenderLosslessError {}
246
247const fn is_symbol_char(c: char) -> bool {
248 matches!(
249 c,
250 '!' | '#'
251 | '$'
252 | '%'
253 | '&'
254 | '*'
255 | '+'
256 | '.'
257 | '/'
258 | '<'
259 | '='
260 | '>'
261 | '?'
262 | '@'
263 | '\\'
264 | '^'
265 | '|'
266 | '-'
267 | '~'
268 | ':'
269 )
270}
271
272fn is_ident_start(c: char) -> bool {
273 c.is_alphabetic() || c == '_'
274}
275
276fn is_ident_char(c: char) -> bool {
277 c.is_alphanumeric() || c == '_' || c == '\''
278}
279
280pub(crate) const TAB_STOP: usize = 8;
281
282struct Lexer<'a> {
283 chars: Vec<char>,
284 src: &'a str,
285 i: usize,
286 byte: usize,
287 line: usize,
288 column: usize,
289 tokens: Vec<Token>,
290 trivia: Vec<Trivia>,
291 errors: Vec<LexError>,
292}
293
294pub fn lex(source: &str) -> LexOutput {
295 let LexWithTriviaOutput { tokens, errors, .. } = lex_with_trivia(source);
296 LexOutput { tokens, errors }
297}
298
299pub fn lex_with_trivia(source: &str) -> LexWithTriviaOutput {
300 let mut lexer = Lexer {
301 chars: source.chars().collect(),
302 src: source,
303 i: 0,
304 byte: 0,
305 line: 1,
306 column: 1,
307 tokens: Vec::new(),
308 trivia: Vec::new(),
309 errors: Vec::new(),
310 };
311 lexer.scan_tokens();
312 let mut trivia = lexer.trivia;
313 add_blank_line_trivia(source, &lexer.tokens, &mut trivia);
314 trivia.sort_by_key(|t| t.start);
315 LexWithTriviaOutput {
316 tokens: lexer.tokens,
317 trivia,
318 errors: lexer.errors,
319 }
320}
321
322pub fn render_lossless(
329 source: &str,
330 tokens: &[Token],
331 trivia: &[Trivia],
332) -> Result<String, RenderLosslessError> {
333 let mut items: Vec<(usize, usize)> = tokens
334 .iter()
335 .filter(|t| {
336 !matches!(
337 t.kind,
338 TokenKind::VLBrace | TokenKind::VRBrace | TokenKind::VSemi
339 )
340 })
341 .map(|t| (t.start, t.end))
342 .chain(
343 trivia
344 .iter()
345 .filter(|t| !matches!(t.kind, TriviaKind::BlankLines(_)))
346 .map(|t| (t.start, t.end)),
347 )
348 .collect();
349 items.sort_unstable();
350 let mut out = String::with_capacity(source.len());
351 let mut prev = 0usize;
352 for (start, end) in items {
353 if start < prev {
354 return Err(RenderLosslessError::OverlappingSpans { start });
355 }
356 let gap = &source[prev..start];
357 if !gap.chars().all(char::is_whitespace) {
358 return Err(RenderLosslessError::UncoveredBytes {
359 start: prev,
360 end: start,
361 text: gap.to_string(),
362 });
363 }
364 out.push_str(gap);
365 out.push_str(&source[start..end]);
366 prev = end;
367 }
368 let tail = &source[prev..];
369 if !tail.chars().all(char::is_whitespace) {
370 return Err(RenderLosslessError::UncoveredTail {
371 start: prev,
372 text: tail.to_string(),
373 });
374 }
375 out.push_str(tail);
376 Ok(out)
377}
378
379fn add_blank_line_trivia(source: &str, tokens: &[Token], trivia: &mut Vec<Trivia>) {
384 let mut spans: Vec<(usize, usize)> = tokens
385 .iter()
386 .map(|t| (t.start, t.end))
387 .chain(trivia.iter().map(|t| (t.start, t.end)))
388 .collect();
389 spans.sort_unstable();
390 let bytes = source.as_bytes();
391 let mut blanks = Vec::new();
392 let mut gap_start = 0usize;
393 let emit_gap = |from: usize, to: usize, out: &mut Vec<Trivia>| {
394 let newlines: Vec<usize> = (from..to).filter(|&i| bytes[i] == b'\n').collect();
399 let count = if from == 0 {
403 newlines.len()
404 } else {
405 newlines.len().saturating_sub(1)
406 };
407 if count == 0 {
408 return;
409 }
410 let region_start = if from == 0 { 0 } else { newlines[0] + 1 };
411 let region_end = newlines[newlines.len() - 1] + 1;
412 let line = source[..region_start].matches('\n').count() + 1;
413 out.push(Trivia {
414 kind: TriviaKind::BlankLines(count),
415 text: String::new(),
416 pos: Pos { line, column: 1 },
417 start: region_start,
418 end: region_end,
419 });
420 };
421 for &(s, e) in &spans {
422 if s > gap_start {
423 emit_gap(gap_start, s, &mut blanks);
424 }
425 gap_start = gap_start.max(e);
426 }
427 if source.len() > gap_start {
428 emit_gap(gap_start, source.len(), &mut blanks);
429 }
430 trivia.extend(blanks);
431}
432
433impl<'a> Lexer<'a> {
434 fn peek(&self) -> Option<char> {
435 self.chars.get(self.i).copied()
436 }
437
438 fn peek_at(&self, n: usize) -> Option<char> {
439 self.chars.get(self.i + n).copied()
440 }
441
442 fn bump(&mut self) -> Option<char> {
443 let c = self.chars.get(self.i).copied()?;
444 self.i += 1;
445 self.byte += c.len_utf8();
446 match c {
447 '\n' => {
448 self.line += 1;
449 self.column = 1;
450 }
451 '\t' => {
452 self.column = ((self.column - 1) / TAB_STOP + 1) * TAB_STOP + 1;
455 }
456 _ => self.column += 1,
457 }
458 Some(c)
459 }
460
461 const fn pos(&self) -> Pos {
462 Pos {
463 line: self.line,
464 column: self.column,
465 }
466 }
467
468 fn push(&mut self, tok: TokenKind, pos: Pos, start: usize) {
471 self.tokens.push(Token {
472 kind: tok,
473 pos,
474 start,
475 end: self.byte,
476 });
477 }
478
479 fn push_trivia(&mut self, kind: TriviaKind, pos: Pos, start: usize) {
480 self.trivia.push(Trivia {
481 kind,
482 text: self.src[start..self.byte].to_string(),
483 pos,
484 start,
485 end: self.byte,
486 });
487 }
488
489 fn error(&mut self, kind: LexErrorKind, pos: Pos) {
490 self.errors.push(LexError { kind, pos });
491 }
492
493 fn scan_tokens(&mut self) {
494 while let Some(c) = self.peek() {
495 let pos = self.pos();
496 let start = self.byte;
497 match c {
498 ' ' | '\t' | '\n' | '\r' => {
499 self.bump();
500 }
501 '(' => {
502 self.bump();
503 self.push(TokenKind::LParen, pos, start);
504 }
505 ')' => {
506 self.bump();
507 self.push(TokenKind::RParen, pos, start);
508 }
509 '[' => {
510 self.bump();
511 self.push(TokenKind::LBracket, pos, start);
512 }
513 ']' => {
514 self.bump();
515 self.push(TokenKind::RBracket, pos, start);
516 }
517 ',' => {
518 self.bump();
519 self.push(TokenKind::Comma, pos, start);
520 }
521 ';' => {
522 self.bump();
523 self.push(TokenKind::Semi, pos, start);
524 }
525 '`' => {
526 self.bump();
527 self.push(TokenKind::Backtick, pos, start);
528 }
529 '{' => {
530 if self.peek_at(1) == Some('-') {
531 self.block_comment(pos);
532 } else {
533 self.bump();
534 self.push(TokenKind::LBrace, pos, start);
535 }
536 }
537 '}' => {
538 self.bump();
539 self.push(TokenKind::RBrace, pos, start);
540 }
541 '#' if self.column == 1
545 && self.peek_at(1).is_some_and(|c| c.is_ascii_lowercase()) =>
546 {
547 while self.peek().is_some_and(|c| c != '\n') {
548 self.bump();
549 }
550 self.push_trivia(TriviaKind::CppDirective, pos, start);
551 }
552 '"' => self.string_lit(pos),
553 '\'' => self.char_lit(pos),
554 c if c.is_ascii_digit() => self.number(pos),
555 c if is_ident_start(c) => self.identifier(pos),
556 c if is_symbol_char(c) => self.operator(pos),
557 _ => {
558 self.bump();
559 self.error(LexErrorKind::UnexpectedCharacter(c), pos);
560 }
561 }
562 }
563 }
564
565 fn block_comment(&mut self, pos: Pos) {
568 let start = self.byte;
569 self.bump(); self.bump(); let mut depth = 1usize;
572 while depth > 0 {
573 match self.peek() {
574 None => {
575 self.error(LexErrorKind::UnterminatedBlockComment, pos);
576 self.push_trivia(TriviaKind::BlockComment, pos, start);
577 return;
578 }
579 Some('{') if self.peek_at(1) == Some('-') => {
580 self.bump();
581 self.bump();
582 depth += 1;
583 }
584 Some('-') if self.peek_at(1) == Some('}') => {
585 self.bump();
586 self.bump();
587 depth -= 1;
588 }
589 Some(_) => {
590 self.bump();
591 }
592 }
593 }
594 self.push_trivia(TriviaKind::BlockComment, pos, start);
595 }
596
597 fn string_lit(&mut self, pos: Pos) {
598 let start = self.byte;
599 self.bump(); let mut value = String::new();
601 loop {
602 match self.peek() {
603 None | Some('\n') => {
604 self.error(LexErrorKind::UnterminatedStringLiteral, pos);
605 break;
606 }
607 Some('"') => {
608 self.bump();
609 break;
610 }
611 Some('\\') => {
612 let escape_pos = self.pos();
613 self.bump();
614 match self.peek() {
615 Some(w) if w.is_whitespace() => {
617 while self.peek().is_some_and(|c| c.is_whitespace()) {
618 self.bump();
619 }
620 if self.peek() == Some('\\') {
621 self.bump();
622 } else {
623 self.error(LexErrorKind::UnterminatedStringGap, pos);
624 break;
625 }
626 }
627 Some(e) => {
628 self.bump();
629 match unescape(e) {
630 Some(c) => value.push(c),
631 None => {
632 self.error(LexErrorKind::InvalidEscapeSequence(e), escape_pos);
633 value.push(e);
634 }
635 }
636 }
637 None => {
638 self.error(LexErrorKind::UnterminatedStringLiteral, pos);
639 break;
640 }
641 }
642 }
643 Some(c) => {
644 self.bump();
645 value.push(c);
646 }
647 }
648 }
649 self.push(TokenKind::StringLit(value), pos, start);
650 }
651
652 fn char_lit(&mut self, pos: Pos) {
656 let start = self.byte;
657 let mut j = self.i + 1;
659 let mut escaped = false;
660 let mut ok = false;
661 let window_end = (self.i + 12).min(self.chars.len());
662 while j < window_end {
663 match self.chars[j] {
664 '\\' if !escaped => escaped = true,
665 '\'' if !escaped => {
666 ok = j > self.i + 1;
667 break;
668 }
669 '\n' => break,
670 _ => escaped = false,
671 }
672 j += 1;
673 }
674 if !ok {
675 self.bump();
676 self.error(LexErrorKind::StraySingleQuote, pos);
677 return;
678 }
679 self.bump(); let mut value = String::new();
681 while self.peek() != Some('\'') {
682 let c = self.bump().unwrap();
683 if c == '\\' {
684 let escape_pos = Pos {
685 line: self.line,
686 column: self.column.saturating_sub(1),
687 };
688 if let Some(e) = self.bump() {
689 match unescape(e) {
690 Some(c) => value.push(c),
691 None => {
692 self.error(LexErrorKind::InvalidEscapeSequence(e), escape_pos);
693 value.push(e);
694 }
695 }
696 }
697 } else {
698 value.push(c);
699 }
700 }
701 self.bump(); if value.chars().count() != 1 {
703 self.error(LexErrorKind::CharacterLiteralWrongLength, pos);
704 }
705 self.push(TokenKind::CharLit(value), pos, start);
706 }
707
708 fn number(&mut self, pos: Pos) {
709 let start = self.byte;
710 let mut text = String::new();
711 if self.peek() == Some('0') && matches!(self.peek_at(1), Some('x' | 'X')) {
712 text.push(self.bump().unwrap());
713 text.push(self.bump().unwrap());
714 let mut has_hex_digit = false;
715 while self
716 .peek()
717 .is_some_and(|c| c.is_ascii_hexdigit() || c == '_')
718 {
719 let c = self.bump().unwrap();
720 has_hex_digit |= c.is_ascii_hexdigit();
721 text.push(c);
722 }
723 if !has_hex_digit {
724 self.error(LexErrorKind::HexLiteralMissingDigits, pos);
725 }
726 self.push(TokenKind::IntLit(text), pos, start);
727 return;
728 }
729 while self.peek().is_some_and(|c| c.is_ascii_digit() || c == '_') {
730 text.push(self.bump().unwrap());
731 }
732 let mut decimal = false;
733 if self.peek() == Some('.') && self.peek_at(1).is_some_and(|c| c.is_ascii_digit()) {
735 decimal = true;
736 text.push(self.bump().unwrap());
737 while self.peek().is_some_and(|c| c.is_ascii_digit() || c == '_') {
738 text.push(self.bump().unwrap());
739 }
740 }
741 if matches!(self.peek(), Some('e' | 'E')) {
742 decimal = true;
743 if self.peek_at(1).is_some_and(|c| c.is_ascii_digit())
744 || (matches!(self.peek_at(1), Some('+' | '-'))
745 && self.peek_at(2).is_some_and(|c| c.is_ascii_digit()))
746 {
747 text.push(self.bump().unwrap());
748 if matches!(self.peek(), Some('+' | '-')) {
749 text.push(self.bump().unwrap());
750 }
751 while self.peek().is_some_and(|c| c.is_ascii_digit()) {
752 text.push(self.bump().unwrap());
753 }
754 } else {
755 text.push(self.bump().unwrap());
756 if matches!(self.peek(), Some('+' | '-')) {
757 text.push(self.bump().unwrap());
758 }
759 self.error(LexErrorKind::DecimalExponentMissingDigits, pos);
760 }
761 }
762 if decimal {
763 self.push(TokenKind::DecimalLit(text), pos, start);
764 } else {
765 self.push(TokenKind::IntLit(text), pos, start);
766 }
767 }
768
769 fn identifier(&mut self, pos: Pos) {
772 let start = self.byte;
773 let mut segments: Vec<String> = Vec::new();
774 loop {
775 let mut seg = String::new();
776 while self.peek().is_some_and(is_ident_char) {
777 seg.push(self.bump().unwrap());
778 }
779 let seg_is_upper = seg.chars().next().is_some_and(|c| c.is_uppercase());
780 segments.push(seg);
781 if seg_is_upper
784 && self.peek() == Some('.')
785 && self.peek_at(1).is_some_and(is_ident_start)
786 {
787 self.bump(); continue;
789 }
790 break;
791 }
792 let name = segments.pop().unwrap();
793 let qualifier = if segments.is_empty() {
794 None
795 } else {
796 Some(segments.join("."))
797 };
798 let tok = if name.chars().next().is_some_and(|c| c.is_uppercase()) {
799 TokenKind::UpperId { qualifier, name }
800 } else {
801 TokenKind::LowerId { qualifier, name }
802 };
803 self.push(tok, pos, start);
804 }
805
806 fn operator(&mut self, pos: Pos) {
807 let start = self.i;
808 let byte_start = self.byte;
809 while self.peek().is_some_and(is_symbol_char) {
810 self.bump();
813 }
814 let text: String = self.chars[start..self.i].iter().collect();
815 if text.len() >= 2 && text.chars().all(|c| c == '-') {
818 while self.peek().is_some_and(|c| c != '\n') {
819 self.bump();
820 }
821 self.push_trivia(TriviaKind::LineComment, pos, byte_start);
822 return;
823 }
824 self.push(TokenKind::Op(text), pos, byte_start);
825 }
826}
827
828const fn unescape(c: char) -> Option<char> {
829 match c {
830 'n' => Some('\n'),
831 't' => Some('\t'),
832 'r' => Some('\r'),
833 '0' => Some('\0'),
834 'a' => Some('\u{07}'),
835 'b' => Some('\u{08}'),
836 'f' => Some('\u{0c}'),
837 'v' => Some('\u{0b}'),
838 '"' => Some('"'),
839 '\'' => Some('\''),
840 '\\' => Some('\\'),
841 '&' => Some('&'),
842 '1'..='9' | 'o' | 'x' | 'A'..='Z' => Some(c),
848 _ => None,
849 }
850}
851
852impl TokenKind {
853 pub const fn keyword(&self) -> Option<&str> {
856 match self {
857 Self::LowerId {
858 qualifier: None,
859 name,
860 } => Some(name.as_str()),
861 _ => None,
862 }
863 }
864
865 pub fn is_keyword(&self, kw: &str) -> bool {
866 self.keyword() == Some(kw)
867 }
868
869 pub fn is_op(&self, op: &str) -> bool {
870 matches!(self, Self::Op(o) if o == op)
871 }
872}
873
874#[cfg(test)]
875mod tests {
876 use super::*;
877
878 fn toks(src: &str) -> Vec<TokenKind> {
879 let (tokens, errors) = lex(src).into_parts();
880 assert!(errors.is_empty(), "lex errors: {errors:?}");
881 tokens.into_iter().map(|t| t.kind).collect()
882 }
883
884 fn lex_error_messages(src: &str) -> Vec<String> {
885 let (_, errors) = lex(src).into_parts();
886 errors.into_iter().map(|e| e.to_string()).collect()
887 }
888
889 fn lower(name: &str) -> TokenKind {
890 TokenKind::LowerId {
891 qualifier: None,
892 name: name.to_string(),
893 }
894 }
895
896 fn upper(name: &str) -> TokenKind {
897 TokenKind::UpperId {
898 qualifier: None,
899 name: name.to_string(),
900 }
901 }
902
903 #[test]
904 fn line_comment_with_keywords_produces_no_tokens() {
905 assert_eq!(toks("-- electing to exercise the option"), vec![]);
906 assert_eq!(toks("--- template Foo"), vec![]);
907 }
908
909 #[test]
910 fn arrow_like_operator_is_not_comment() {
911 assert_eq!(
912 toks("a --> b"),
913 vec![lower("a"), TokenKind::Op("-->".into()), lower("b")]
914 );
915 }
916
917 #[test]
918 fn nested_block_comment() {
919 assert_eq!(toks("{- outer {- inner -} still -} x"), vec![lower("x")]);
920 }
921
922 #[test]
923 fn string_with_keyword_and_escapes() {
924 assert_eq!(
925 toks(r#""template \"Foo\" \n""#),
926 vec![TokenKind::StringLit("template \"Foo\" \n".into())]
927 );
928 }
929
930 #[test]
931 fn qualified_identifiers() {
932 assert_eq!(
933 toks("DA.Set.fromList Map.Map foo"),
934 vec![
935 TokenKind::LowerId {
936 qualifier: Some("DA.Set".into()),
937 name: "fromList".into()
938 },
939 TokenKind::UpperId {
940 qualifier: Some("Map".into()),
941 name: "Map".into()
942 },
943 lower("foo"),
944 ]
945 );
946 }
947
948 #[test]
949 fn numbers() {
950 assert_eq!(
951 toks("42 1.5 0x1F 2e3 1_000"),
952 vec![
953 TokenKind::IntLit("42".into()),
954 TokenKind::DecimalLit("1.5".into()),
955 TokenKind::IntLit("0x1F".into()),
956 TokenKind::DecimalLit("2e3".into()),
957 TokenKind::IntLit("1_000".into()),
958 ]
959 );
960 }
961
962 #[test]
963 fn malformed_hex_literal_reports_error() {
964 assert_eq!(
965 lex_error_messages("0x 0x_"),
966 vec![
967 "hex literal requires at least one digit",
968 "hex literal requires at least one digit",
969 ]
970 );
971 }
972
973 #[test]
974 fn malformed_decimal_exponent_reports_error() {
975 assert_eq!(
976 lex_error_messages("1e 1e+ 1e-"),
977 vec![
978 "decimal exponent requires at least one digit",
979 "decimal exponent requires at least one digit",
980 "decimal exponent requires at least one digit",
981 ]
982 );
983 }
984
985 #[test]
986 fn enum_from_to_is_not_decimal() {
987 assert_eq!(
988 toks("[1..5]"),
989 vec![
990 TokenKind::LBracket,
991 TokenKind::IntLit("1".into()),
992 TokenKind::Op("..".into()),
993 TokenKind::IntLit("5".into()),
994 TokenKind::RBracket,
995 ]
996 );
997 }
998
999 #[test]
1000 fn primes_stay_in_identifier_and_char_lit_works() {
1001 assert_eq!(
1002 toks(r"foo' 'a' '\n'"),
1003 vec![
1004 lower("foo'"),
1005 TokenKind::CharLit("a".into()),
1006 TokenKind::CharLit("\n".into())
1007 ]
1008 );
1009 }
1010
1011 #[test]
1012 fn invalid_escape_sequences_report_errors() {
1013 assert_eq!(
1014 lex_error_messages(r#""\q" '\q'"#),
1015 vec!["invalid escape sequence \\q", "invalid escape sequence \\q"]
1016 );
1017 }
1018
1019 #[test]
1020 fn multi_character_char_literal_reports_error() {
1021 let (tokens, errors) = lex("'ab'").into_parts();
1022 assert_eq!(
1023 tokens.iter().map(|t| t.kind.clone()).collect::<Vec<_>>(),
1024 vec![TokenKind::CharLit("ab".into())]
1025 );
1026 assert_eq!(
1027 errors.iter().map(|e| e.to_string()).collect::<Vec<_>>(),
1028 vec!["character literal must contain exactly one character".to_string()]
1029 );
1030 }
1031
1032 #[test]
1033 fn operators_and_punctuation() {
1034 assert_eq!(
1035 toks("x <- f (y, z) `div` 2"),
1036 vec![
1037 lower("x"),
1038 TokenKind::Op("<-".into()),
1039 lower("f"),
1040 TokenKind::LParen,
1041 lower("y"),
1042 TokenKind::Comma,
1043 lower("z"),
1044 TokenKind::RParen,
1045 TokenKind::Backtick,
1046 lower("div"),
1047 TokenKind::Backtick,
1048 TokenKind::IntLit("2".into()),
1049 ]
1050 );
1051 }
1052
1053 #[test]
1054 fn spans_are_one_based() {
1055 let (tokens, _) = lex("ab\n cd").into_parts();
1056 assert_eq!(tokens[0].pos, Pos { line: 1, column: 1 });
1057 assert_eq!(tokens[1].pos, Pos { line: 2, column: 3 });
1058 }
1059
1060 #[test]
1061 fn tab_advances_to_stop() {
1062 let (tokens, _) = lex("\tx").into_parts();
1063 assert_eq!(tokens[0].pos, Pos { line: 1, column: 9 });
1064 }
1065
1066 #[test]
1067 fn unterminated_string_is_error_not_hang() {
1068 let (_, errors) = lex("x = \"oops\ny").into_parts();
1069 assert_eq!(errors.len(), 1);
1070 }
1071
1072 #[test]
1073 fn unterminated_block_comment_is_error_not_hang() {
1074 let (_, errors) = lex("{- never closed").into_parts();
1075 assert_eq!(errors.len(), 1);
1076 }
1077
1078 fn trivia_of(src: &str) -> Vec<Trivia> {
1079 let (_, trivia, _) = lex_with_trivia(src).into_parts();
1080 trivia
1081 }
1082
1083 fn assert_round_trip(src: &str) {
1086 let (tokens, trivia, errors) = lex_with_trivia(src).into_parts();
1087 assert!(errors.is_empty(), "lex errors: {errors:?}");
1088 assert_eq!(
1089 render_lossless(src, &tokens, &trivia).as_deref(),
1090 Ok(src),
1091 "round trip failed for {src:?}"
1092 );
1093 }
1094
1095 #[test]
1096 fn line_comment_becomes_trivia_with_exact_text_and_span() {
1097 let src = "x = 1 -- electing to exercise\ny = 2\n";
1098 let trivia = trivia_of(src);
1099 assert_eq!(trivia.len(), 1);
1100 assert_eq!(trivia[0].kind, TriviaKind::LineComment);
1101 assert_eq!(trivia[0].text, "-- electing to exercise");
1102 assert_eq!(&src[trivia[0].start..trivia[0].end], trivia[0].text);
1103 assert_eq!(trivia[0].pos, Pos { line: 1, column: 7 });
1104 }
1105
1106 #[test]
1107 fn nested_block_comment_becomes_one_trivia() {
1108 let src = "{- outer {- inner -} still -} x";
1109 let trivia = trivia_of(src);
1110 assert_eq!(trivia.len(), 1);
1111 assert_eq!(trivia[0].kind, TriviaKind::BlockComment);
1112 assert_eq!(trivia[0].text, "{- outer {- inner -} still -}");
1113 }
1114
1115 #[test]
1116 fn unterminated_block_comment_still_yields_trivia_to_eof() {
1117 let (_, trivia, errors) = lex_with_trivia("x {- never closed").into_parts();
1118 assert_eq!(errors.len(), 1);
1119 assert_eq!(trivia.len(), 1);
1120 assert_eq!(trivia[0].text, "{- never closed");
1121 }
1122
1123 #[test]
1124 fn blank_lines_between_items_counted() {
1125 let src = "x = 1\n\n\ny = 2\n";
1126 let trivia = trivia_of(src);
1127 assert_eq!(trivia.len(), 1);
1128 assert_eq!(trivia[0].kind, TriviaKind::BlankLines(2));
1129 assert_eq!(trivia[0].pos, Pos { line: 2, column: 1 });
1130 }
1131
1132 #[test]
1133 fn blank_line_at_file_start_counted() {
1134 let trivia = trivia_of("\nx = 1\n");
1135 assert_eq!(trivia.len(), 1);
1136 assert_eq!(trivia[0].kind, TriviaKind::BlankLines(1));
1137 assert_eq!(trivia[0].pos, Pos { line: 1, column: 1 });
1138 }
1139
1140 #[test]
1141 fn blank_looking_lines_inside_block_comment_are_not_blank_trivia() {
1142 let src = "x = 1 {- a\n\nb -}\ny = 2\n";
1143 let trivia = trivia_of(src);
1144 assert_eq!(trivia.len(), 1, "{trivia:?}");
1145 assert_eq!(trivia[0].kind, TriviaKind::BlockComment);
1146 }
1147
1148 #[test]
1149 fn blank_line_between_comments_counted() {
1150 let src = "-- a\n\n-- b\nx = 1\n";
1151 let kinds: Vec<_> = trivia_of(src).into_iter().map(|t| t.kind).collect();
1152 assert_eq!(
1153 kinds,
1154 vec![
1155 TriviaKind::LineComment,
1156 TriviaKind::BlankLines(1),
1157 TriviaKind::LineComment,
1158 ]
1159 );
1160 }
1161
1162 #[test]
1163 fn cpp_directive_becomes_trivia() {
1164 let src = "#ifdef DAML_BIGNUMERIC\nx = 1\n#endif\n";
1165 let trivia = trivia_of(src);
1166 assert_eq!(trivia.len(), 2);
1167 assert!(trivia.iter().all(|t| t.kind == TriviaKind::CppDirective));
1168 assert_eq!(trivia[0].text, "#ifdef DAML_BIGNUMERIC");
1169 }
1170
1171 #[test]
1172 fn round_trip_is_byte_identical() {
1173 assert_round_trip("module M where\n\n-- doc\nf : Int -> Int\nf x = x + 1\n");
1174 assert_round_trip("x = \"tem\\\"plate \\n\" {- block {- nested -} -}\r\ny = 'a'\r\n");
1175 assert_round_trip("\tärger = [1..5] -- ütf\n");
1176 assert_round_trip("s = \"gap \\ \\ here\"\n");
1177 assert_round_trip("#ifdef X\nf = 0x1F\n#endif\n");
1178 assert_round_trip("\n\n \nf = 1 \n ");
1179 assert_round_trip("");
1180 }
1181
1182 #[test]
1183 fn render_lossless_detects_lost_bytes() {
1184 let src = "x = 1 -- comment\n";
1185 let (tokens, mut trivia, _) = lex_with_trivia(src).into_parts();
1186 trivia.clear(); assert!(render_lossless(src, &tokens, &trivia).is_err());
1188 }
1189
1190 #[test]
1191 fn unicode_identifier() {
1192 assert_eq!(
1193 toks("ärger = 1"),
1194 vec![
1195 lower("ärger"),
1196 TokenKind::Op("=".into()),
1197 TokenKind::IntLit("1".into())
1198 ]
1199 );
1200 let _ = upper("Ülf"); }
1202}