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