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 Tok {
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 tok: Tok,
53 pub pos: Pos,
54 pub start: usize,
57 pub end: usize,
59}
60
61impl Token {
62 pub const fn is_virtual(&self) -> bool {
65 matches!(self.tok, Tok::VLBrace | Tok::VRBrace | Tok::VSemi)
66 }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
74pub enum TriviaKind {
75 LineComment,
77 BlockComment,
79 CppDirective,
81 BlankLines(usize),
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct Trivia {
87 pub kind: TriviaKind,
88 pub text: String,
90 pub pos: Pos,
91 pub start: usize,
92 pub end: usize,
93}
94
95#[derive(Debug, Clone)]
98pub struct LexError {
99 pub message: String,
100 pub pos: Pos,
101}
102
103const fn is_symbol_char(c: char) -> bool {
104 matches!(
105 c,
106 '!' | '#'
107 | '$'
108 | '%'
109 | '&'
110 | '*'
111 | '+'
112 | '.'
113 | '/'
114 | '<'
115 | '='
116 | '>'
117 | '?'
118 | '@'
119 | '\\'
120 | '^'
121 | '|'
122 | '-'
123 | '~'
124 | ':'
125 )
126}
127
128fn is_ident_start(c: char) -> bool {
129 c.is_alphabetic() || c == '_'
130}
131
132fn is_ident_char(c: char) -> bool {
133 c.is_alphanumeric() || c == '_' || c == '\''
134}
135
136pub(crate) const TAB_STOP: usize = 8;
137
138struct Lexer<'a> {
139 chars: Vec<char>,
140 src: &'a str,
141 i: usize,
142 byte: usize,
143 line: usize,
144 column: usize,
145 tokens: Vec<Token>,
146 trivia: Vec<Trivia>,
147 errors: Vec<LexError>,
148}
149
150pub fn lex(source: &str) -> (Vec<Token>, Vec<LexError>) {
151 let (tokens, _, errors) = lex_with_trivia(source);
152 (tokens, errors)
153}
154
155pub fn lex_with_trivia(source: &str) -> (Vec<Token>, Vec<Trivia>, Vec<LexError>) {
156 let mut lexer = Lexer {
157 chars: source.chars().collect(),
158 src: source,
159 i: 0,
160 byte: 0,
161 line: 1,
162 column: 1,
163 tokens: Vec::new(),
164 trivia: Vec::new(),
165 errors: Vec::new(),
166 };
167 lexer.scan_tokens();
168 let mut trivia = lexer.trivia;
169 add_blank_line_trivia(source, &lexer.tokens, &mut trivia);
170 trivia.sort_by_key(|t| t.start);
171 (lexer.tokens, trivia, lexer.errors)
172}
173
174pub fn render_lossless(
181 source: &str,
182 tokens: &[Token],
183 trivia: &[Trivia],
184) -> Result<String, String> {
185 let mut items: Vec<(usize, usize)> = tokens
186 .iter()
187 .filter(|t| !matches!(t.tok, Tok::VLBrace | Tok::VRBrace | Tok::VSemi))
188 .map(|t| (t.start, t.end))
189 .chain(
190 trivia
191 .iter()
192 .filter(|t| !matches!(t.kind, TriviaKind::BlankLines(_)))
193 .map(|t| (t.start, t.end)),
194 )
195 .collect();
196 items.sort_unstable();
197 let mut out = String::with_capacity(source.len());
198 let mut prev = 0usize;
199 for (start, end) in items {
200 if start < prev {
201 return Err(format!("overlapping spans at byte {start}"));
202 }
203 let gap = &source[prev..start];
204 if !gap.chars().all(char::is_whitespace) {
205 return Err(format!(
206 "bytes {prev}..{start} lost (not covered by any token/trivia): {gap:?}"
207 ));
208 }
209 out.push_str(gap);
210 out.push_str(&source[start..end]);
211 prev = end;
212 }
213 let tail = &source[prev..];
214 if !tail.chars().all(char::is_whitespace) {
215 return Err(format!("bytes {prev}.. lost at EOF: {tail:?}"));
216 }
217 out.push_str(tail);
218 Ok(out)
219}
220
221fn add_blank_line_trivia(source: &str, tokens: &[Token], trivia: &mut Vec<Trivia>) {
226 let mut spans: Vec<(usize, usize)> = tokens
227 .iter()
228 .map(|t| (t.start, t.end))
229 .chain(trivia.iter().map(|t| (t.start, t.end)))
230 .collect();
231 spans.sort_unstable();
232 let bytes = source.as_bytes();
233 let mut blanks = Vec::new();
234 let mut gap_start = 0usize;
235 let emit_gap = |from: usize, to: usize, out: &mut Vec<Trivia>| {
236 let newlines: Vec<usize> = (from..to).filter(|&i| bytes[i] == b'\n').collect();
241 let count = if from == 0 {
245 newlines.len()
246 } else {
247 newlines.len().saturating_sub(1)
248 };
249 if count == 0 {
250 return;
251 }
252 let region_start = if from == 0 { 0 } else { newlines[0] + 1 };
253 let region_end = newlines[newlines.len() - 1] + 1;
254 let line = source[..region_start].matches('\n').count() + 1;
255 out.push(Trivia {
256 kind: TriviaKind::BlankLines(count),
257 text: String::new(),
258 pos: Pos { line, column: 1 },
259 start: region_start,
260 end: region_end,
261 });
262 };
263 for &(s, e) in &spans {
264 if s > gap_start {
265 emit_gap(gap_start, s, &mut blanks);
266 }
267 gap_start = gap_start.max(e);
268 }
269 if source.len() > gap_start {
270 emit_gap(gap_start, source.len(), &mut blanks);
271 }
272 trivia.extend(blanks);
273}
274
275impl<'a> Lexer<'a> {
276 fn peek(&self) -> Option<char> {
277 self.chars.get(self.i).copied()
278 }
279
280 fn peek_at(&self, n: usize) -> Option<char> {
281 self.chars.get(self.i + n).copied()
282 }
283
284 fn bump(&mut self) -> Option<char> {
285 let c = self.chars.get(self.i).copied()?;
286 self.i += 1;
287 self.byte += c.len_utf8();
288 match c {
289 '\n' => {
290 self.line += 1;
291 self.column = 1;
292 }
293 '\t' => {
294 self.column = ((self.column - 1) / TAB_STOP + 1) * TAB_STOP + 1;
297 }
298 _ => self.column += 1,
299 }
300 Some(c)
301 }
302
303 const fn pos(&self) -> Pos {
304 Pos {
305 line: self.line,
306 column: self.column,
307 }
308 }
309
310 fn push(&mut self, tok: Tok, pos: Pos, start: usize) {
313 self.tokens.push(Token {
314 tok,
315 pos,
316 start,
317 end: self.byte,
318 });
319 }
320
321 fn push_trivia(&mut self, kind: TriviaKind, pos: Pos, start: usize) {
322 self.trivia.push(Trivia {
323 kind,
324 text: self.src[start..self.byte].to_string(),
325 pos,
326 start,
327 end: self.byte,
328 });
329 }
330
331 fn error(&mut self, message: impl Into<String>, pos: Pos) {
332 self.errors.push(LexError {
333 message: message.into(),
334 pos,
335 });
336 }
337
338 fn scan_tokens(&mut self) {
339 while let Some(c) = self.peek() {
340 let pos = self.pos();
341 let start = self.byte;
342 match c {
343 ' ' | '\t' | '\n' | '\r' => {
344 self.bump();
345 }
346 '(' => {
347 self.bump();
348 self.push(Tok::LParen, pos, start);
349 }
350 ')' => {
351 self.bump();
352 self.push(Tok::RParen, pos, start);
353 }
354 '[' => {
355 self.bump();
356 self.push(Tok::LBracket, pos, start);
357 }
358 ']' => {
359 self.bump();
360 self.push(Tok::RBracket, pos, start);
361 }
362 ',' => {
363 self.bump();
364 self.push(Tok::Comma, pos, start);
365 }
366 ';' => {
367 self.bump();
368 self.push(Tok::Semi, pos, start);
369 }
370 '`' => {
371 self.bump();
372 self.push(Tok::Backtick, pos, start);
373 }
374 '{' => {
375 if self.peek_at(1) == Some('-') {
376 self.block_comment(pos);
377 } else {
378 self.bump();
379 self.push(Tok::LBrace, pos, start);
380 }
381 }
382 '}' => {
383 self.bump();
384 self.push(Tok::RBrace, pos, start);
385 }
386 '#' if self.column == 1
390 && self.peek_at(1).is_some_and(|c| c.is_ascii_lowercase()) =>
391 {
392 while self.peek().is_some_and(|c| c != '\n') {
393 self.bump();
394 }
395 self.push_trivia(TriviaKind::CppDirective, pos, start);
396 }
397 '"' => self.string_lit(pos),
398 '\'' => self.char_lit(pos),
399 c if c.is_ascii_digit() => self.number(pos),
400 c if is_ident_start(c) => self.identifier(pos),
401 c if is_symbol_char(c) => self.operator(pos),
402 _ => {
403 self.bump();
404 self.error(format!("unexpected character '{c}'"), pos);
405 }
406 }
407 }
408 }
409
410 fn block_comment(&mut self, pos: Pos) {
413 let start = self.byte;
414 self.bump(); self.bump(); let mut depth = 1usize;
417 while depth > 0 {
418 match self.peek() {
419 None => {
420 self.error("unterminated block comment", pos);
421 self.push_trivia(TriviaKind::BlockComment, pos, start);
422 return;
423 }
424 Some('{') if self.peek_at(1) == Some('-') => {
425 self.bump();
426 self.bump();
427 depth += 1;
428 }
429 Some('-') if self.peek_at(1) == Some('}') => {
430 self.bump();
431 self.bump();
432 depth -= 1;
433 }
434 Some(_) => {
435 self.bump();
436 }
437 }
438 }
439 self.push_trivia(TriviaKind::BlockComment, pos, start);
440 }
441
442 fn string_lit(&mut self, pos: Pos) {
443 let start = self.byte;
444 self.bump(); let mut value = String::new();
446 loop {
447 match self.peek() {
448 None | Some('\n') => {
449 self.error("unterminated string literal", pos);
450 break;
451 }
452 Some('"') => {
453 self.bump();
454 break;
455 }
456 Some('\\') => {
457 let escape_pos = self.pos();
458 self.bump();
459 match self.peek() {
460 Some(w) if w.is_whitespace() => {
462 while self.peek().is_some_and(|c| c.is_whitespace()) {
463 self.bump();
464 }
465 if self.peek() == Some('\\') {
466 self.bump();
467 } else {
468 self.error("unterminated string gap", pos);
469 break;
470 }
471 }
472 Some(e) => {
473 self.bump();
474 match unescape(e) {
475 Some(c) => value.push(c),
476 None => {
477 self.error(
478 format!("invalid escape sequence \\{e}"),
479 escape_pos,
480 );
481 value.push(e);
482 }
483 }
484 }
485 None => {
486 self.error("unterminated string literal", pos);
487 break;
488 }
489 }
490 }
491 Some(c) => {
492 self.bump();
493 value.push(c);
494 }
495 }
496 }
497 self.push(Tok::StringLit(value), pos, start);
498 }
499
500 fn char_lit(&mut self, pos: Pos) {
504 let start = self.byte;
505 let mut j = self.i + 1;
507 let mut escaped = false;
508 let mut ok = false;
509 let window_end = (self.i + 12).min(self.chars.len());
510 while j < window_end {
511 match self.chars[j] {
512 '\\' if !escaped => escaped = true,
513 '\'' if !escaped => {
514 ok = j > self.i + 1;
515 break;
516 }
517 '\n' => break,
518 _ => escaped = false,
519 }
520 j += 1;
521 }
522 if !ok {
523 self.bump();
524 self.error("stray single quote", pos);
525 return;
526 }
527 self.bump(); let mut value = String::new();
529 while self.peek() != Some('\'') {
530 let c = self.bump().unwrap();
531 if c == '\\' {
532 let escape_pos = Pos {
533 line: self.line,
534 column: self.column.saturating_sub(1),
535 };
536 if let Some(e) = self.bump() {
537 match unescape(e) {
538 Some(c) => value.push(c),
539 None => {
540 self.error(format!("invalid escape sequence \\{e}"), escape_pos);
541 value.push(e);
542 }
543 }
544 }
545 } else {
546 value.push(c);
547 }
548 }
549 self.bump(); if value.chars().count() != 1 {
551 self.error("character literal must contain exactly one character", pos);
552 }
553 self.push(Tok::CharLit(value), pos, start);
554 }
555
556 fn number(&mut self, pos: Pos) {
557 let start = self.byte;
558 let mut text = String::new();
559 if self.peek() == Some('0') && matches!(self.peek_at(1), Some('x' | 'X')) {
560 text.push(self.bump().unwrap());
561 text.push(self.bump().unwrap());
562 let mut has_hex_digit = false;
563 while self
564 .peek()
565 .is_some_and(|c| c.is_ascii_hexdigit() || c == '_')
566 {
567 let c = self.bump().unwrap();
568 has_hex_digit |= c.is_ascii_hexdigit();
569 text.push(c);
570 }
571 if !has_hex_digit {
572 self.error("hex literal requires at least one digit", pos);
573 }
574 self.push(Tok::IntLit(text), pos, start);
575 return;
576 }
577 while self.peek().is_some_and(|c| c.is_ascii_digit() || c == '_') {
578 text.push(self.bump().unwrap());
579 }
580 let mut decimal = false;
581 if self.peek() == Some('.') && self.peek_at(1).is_some_and(|c| c.is_ascii_digit()) {
583 decimal = true;
584 text.push(self.bump().unwrap());
585 while self.peek().is_some_and(|c| c.is_ascii_digit() || c == '_') {
586 text.push(self.bump().unwrap());
587 }
588 }
589 if matches!(self.peek(), Some('e' | 'E')) {
590 decimal = true;
591 if self.peek_at(1).is_some_and(|c| c.is_ascii_digit())
592 || (matches!(self.peek_at(1), Some('+' | '-'))
593 && self.peek_at(2).is_some_and(|c| c.is_ascii_digit()))
594 {
595 text.push(self.bump().unwrap());
596 if matches!(self.peek(), Some('+' | '-')) {
597 text.push(self.bump().unwrap());
598 }
599 while self.peek().is_some_and(|c| c.is_ascii_digit()) {
600 text.push(self.bump().unwrap());
601 }
602 } else {
603 text.push(self.bump().unwrap());
604 if matches!(self.peek(), Some('+' | '-')) {
605 text.push(self.bump().unwrap());
606 }
607 self.error("decimal exponent requires at least one digit", pos);
608 }
609 }
610 if decimal {
611 self.push(Tok::DecimalLit(text), pos, start);
612 } else {
613 self.push(Tok::IntLit(text), pos, start);
614 }
615 }
616
617 fn identifier(&mut self, pos: Pos) {
620 let start = self.byte;
621 let mut segments: Vec<String> = Vec::new();
622 loop {
623 let mut seg = String::new();
624 while self.peek().is_some_and(is_ident_char) {
625 seg.push(self.bump().unwrap());
626 }
627 let seg_is_upper = seg.chars().next().is_some_and(|c| c.is_uppercase());
628 segments.push(seg);
629 if seg_is_upper
632 && self.peek() == Some('.')
633 && self.peek_at(1).is_some_and(is_ident_start)
634 {
635 self.bump(); continue;
637 }
638 break;
639 }
640 let name = segments.pop().unwrap();
641 let qualifier = if segments.is_empty() {
642 None
643 } else {
644 Some(segments.join("."))
645 };
646 let tok = if name.chars().next().is_some_and(|c| c.is_uppercase()) {
647 Tok::UpperId { qualifier, name }
648 } else {
649 Tok::LowerId { qualifier, name }
650 };
651 self.push(tok, pos, start);
652 }
653
654 fn operator(&mut self, pos: Pos) {
655 let start = self.i;
656 let byte_start = self.byte;
657 while self.peek().is_some_and(is_symbol_char) {
658 self.bump();
661 }
662 let text: String = self.chars[start..self.i].iter().collect();
663 if text.len() >= 2 && text.chars().all(|c| c == '-') {
666 while self.peek().is_some_and(|c| c != '\n') {
667 self.bump();
668 }
669 self.push_trivia(TriviaKind::LineComment, pos, byte_start);
670 return;
671 }
672 self.push(Tok::Op(text), pos, byte_start);
673 }
674}
675
676const fn unescape(c: char) -> Option<char> {
677 match c {
678 'n' => Some('\n'),
679 't' => Some('\t'),
680 'r' => Some('\r'),
681 '0' => Some('\0'),
682 'a' => Some('\u{07}'),
683 'b' => Some('\u{08}'),
684 'f' => Some('\u{0c}'),
685 'v' => Some('\u{0b}'),
686 '"' => Some('"'),
687 '\'' => Some('\''),
688 '\\' => Some('\\'),
689 '&' => Some('&'),
690 '1'..='9' | 'o' | 'x' | 'A'..='Z' => Some(c),
696 _ => None,
697 }
698}
699
700impl Tok {
701 pub const fn keyword(&self) -> Option<&str> {
704 match self {
705 Self::LowerId {
706 qualifier: None,
707 name,
708 } => Some(name.as_str()),
709 _ => None,
710 }
711 }
712
713 pub fn is_keyword(&self, kw: &str) -> bool {
714 self.keyword() == Some(kw)
715 }
716
717 pub fn is_op(&self, op: &str) -> bool {
718 matches!(self, Self::Op(o) if o == op)
719 }
720}
721
722#[cfg(test)]
723mod tests {
724 use super::*;
725
726 fn toks(src: &str) -> Vec<Tok> {
727 let (tokens, errors) = lex(src);
728 assert!(errors.is_empty(), "lex errors: {errors:?}");
729 tokens.into_iter().map(|t| t.tok).collect()
730 }
731
732 fn lex_error_messages(src: &str) -> Vec<String> {
733 let (_, errors) = lex(src);
734 errors.into_iter().map(|e| e.message).collect()
735 }
736
737 fn lower(name: &str) -> Tok {
738 Tok::LowerId {
739 qualifier: None,
740 name: name.to_string(),
741 }
742 }
743
744 fn upper(name: &str) -> Tok {
745 Tok::UpperId {
746 qualifier: None,
747 name: name.to_string(),
748 }
749 }
750
751 #[test]
752 fn line_comment_with_keywords_produces_no_tokens() {
753 assert_eq!(toks("-- electing to exercise the option"), vec![]);
754 assert_eq!(toks("--- template Foo"), vec![]);
755 }
756
757 #[test]
758 fn arrow_like_operator_is_not_comment() {
759 assert_eq!(
760 toks("a --> b"),
761 vec![lower("a"), Tok::Op("-->".into()), lower("b")]
762 );
763 }
764
765 #[test]
766 fn nested_block_comment() {
767 assert_eq!(toks("{- outer {- inner -} still -} x"), vec![lower("x")]);
768 }
769
770 #[test]
771 fn string_with_keyword_and_escapes() {
772 assert_eq!(
773 toks(r#""template \"Foo\" \n""#),
774 vec![Tok::StringLit("template \"Foo\" \n".into())]
775 );
776 }
777
778 #[test]
779 fn qualified_identifiers() {
780 assert_eq!(
781 toks("DA.Set.fromList Map.Map foo"),
782 vec![
783 Tok::LowerId {
784 qualifier: Some("DA.Set".into()),
785 name: "fromList".into()
786 },
787 Tok::UpperId {
788 qualifier: Some("Map".into()),
789 name: "Map".into()
790 },
791 lower("foo"),
792 ]
793 );
794 }
795
796 #[test]
797 fn numbers() {
798 assert_eq!(
799 toks("42 1.5 0x1F 2e3 1_000"),
800 vec![
801 Tok::IntLit("42".into()),
802 Tok::DecimalLit("1.5".into()),
803 Tok::IntLit("0x1F".into()),
804 Tok::DecimalLit("2e3".into()),
805 Tok::IntLit("1_000".into()),
806 ]
807 );
808 }
809
810 #[test]
811 fn malformed_hex_literal_reports_error() {
812 assert_eq!(
813 lex_error_messages("0x 0x_"),
814 vec![
815 "hex literal requires at least one digit",
816 "hex literal requires at least one digit",
817 ]
818 );
819 }
820
821 #[test]
822 fn malformed_decimal_exponent_reports_error() {
823 assert_eq!(
824 lex_error_messages("1e 1e+ 1e-"),
825 vec![
826 "decimal exponent requires at least one digit",
827 "decimal exponent requires at least one digit",
828 "decimal exponent requires at least one digit",
829 ]
830 );
831 }
832
833 #[test]
834 fn enum_from_to_is_not_decimal() {
835 assert_eq!(
836 toks("[1..5]"),
837 vec![
838 Tok::LBracket,
839 Tok::IntLit("1".into()),
840 Tok::Op("..".into()),
841 Tok::IntLit("5".into()),
842 Tok::RBracket,
843 ]
844 );
845 }
846
847 #[test]
848 fn primes_stay_in_identifier_and_char_lit_works() {
849 assert_eq!(
850 toks(r"foo' 'a' '\n'"),
851 vec![
852 lower("foo'"),
853 Tok::CharLit("a".into()),
854 Tok::CharLit("\n".into())
855 ]
856 );
857 }
858
859 #[test]
860 fn invalid_escape_sequences_report_errors() {
861 assert_eq!(
862 lex_error_messages(r#""\q" '\q'"#),
863 vec!["invalid escape sequence \\q", "invalid escape sequence \\q"]
864 );
865 }
866
867 #[test]
868 fn multi_character_char_literal_reports_error() {
869 let (tokens, errors) = lex("'ab'");
870 assert_eq!(
871 tokens.iter().map(|t| t.tok.clone()).collect::<Vec<_>>(),
872 vec![Tok::CharLit("ab".into())]
873 );
874 assert_eq!(
875 errors
876 .iter()
877 .map(|e| e.message.as_str())
878 .collect::<Vec<_>>(),
879 vec!["character literal must contain exactly one character"]
880 );
881 }
882
883 #[test]
884 fn operators_and_punctuation() {
885 assert_eq!(
886 toks("x <- f (y, z) `div` 2"),
887 vec![
888 lower("x"),
889 Tok::Op("<-".into()),
890 lower("f"),
891 Tok::LParen,
892 lower("y"),
893 Tok::Comma,
894 lower("z"),
895 Tok::RParen,
896 Tok::Backtick,
897 lower("div"),
898 Tok::Backtick,
899 Tok::IntLit("2".into()),
900 ]
901 );
902 }
903
904 #[test]
905 fn spans_are_one_based() {
906 let (tokens, _) = lex("ab\n cd");
907 assert_eq!(tokens[0].pos, Pos { line: 1, column: 1 });
908 assert_eq!(tokens[1].pos, Pos { line: 2, column: 3 });
909 }
910
911 #[test]
912 fn tab_advances_to_stop() {
913 let (tokens, _) = lex("\tx");
914 assert_eq!(tokens[0].pos, Pos { line: 1, column: 9 });
915 }
916
917 #[test]
918 fn unterminated_string_is_error_not_hang() {
919 let (_, errors) = lex("x = \"oops\ny");
920 assert_eq!(errors.len(), 1);
921 }
922
923 #[test]
924 fn unterminated_block_comment_is_error_not_hang() {
925 let (_, errors) = lex("{- never closed");
926 assert_eq!(errors.len(), 1);
927 }
928
929 fn trivia_of(src: &str) -> Vec<Trivia> {
930 let (_, trivia, _) = lex_with_trivia(src);
931 trivia
932 }
933
934 fn assert_round_trip(src: &str) {
937 let (tokens, trivia, errors) = lex_with_trivia(src);
938 assert!(errors.is_empty(), "lex errors: {errors:?}");
939 assert_eq!(
940 render_lossless(src, &tokens, &trivia).as_deref(),
941 Ok(src),
942 "round trip failed for {src:?}"
943 );
944 }
945
946 #[test]
947 fn line_comment_becomes_trivia_with_exact_text_and_span() {
948 let src = "x = 1 -- electing to exercise\ny = 2\n";
949 let trivia = trivia_of(src);
950 assert_eq!(trivia.len(), 1);
951 assert_eq!(trivia[0].kind, TriviaKind::LineComment);
952 assert_eq!(trivia[0].text, "-- electing to exercise");
953 assert_eq!(&src[trivia[0].start..trivia[0].end], trivia[0].text);
954 assert_eq!(trivia[0].pos, Pos { line: 1, column: 7 });
955 }
956
957 #[test]
958 fn nested_block_comment_becomes_one_trivia() {
959 let src = "{- outer {- inner -} still -} x";
960 let trivia = trivia_of(src);
961 assert_eq!(trivia.len(), 1);
962 assert_eq!(trivia[0].kind, TriviaKind::BlockComment);
963 assert_eq!(trivia[0].text, "{- outer {- inner -} still -}");
964 }
965
966 #[test]
967 fn unterminated_block_comment_still_yields_trivia_to_eof() {
968 let (_, trivia, errors) = lex_with_trivia("x {- never closed");
969 assert_eq!(errors.len(), 1);
970 assert_eq!(trivia.len(), 1);
971 assert_eq!(trivia[0].text, "{- never closed");
972 }
973
974 #[test]
975 fn blank_lines_between_items_counted() {
976 let src = "x = 1\n\n\ny = 2\n";
977 let trivia = trivia_of(src);
978 assert_eq!(trivia.len(), 1);
979 assert_eq!(trivia[0].kind, TriviaKind::BlankLines(2));
980 assert_eq!(trivia[0].pos, Pos { line: 2, column: 1 });
981 }
982
983 #[test]
984 fn blank_line_at_file_start_counted() {
985 let trivia = trivia_of("\nx = 1\n");
986 assert_eq!(trivia.len(), 1);
987 assert_eq!(trivia[0].kind, TriviaKind::BlankLines(1));
988 assert_eq!(trivia[0].pos, Pos { line: 1, column: 1 });
989 }
990
991 #[test]
992 fn blank_looking_lines_inside_block_comment_are_not_blank_trivia() {
993 let src = "x = 1 {- a\n\nb -}\ny = 2\n";
994 let trivia = trivia_of(src);
995 assert_eq!(trivia.len(), 1, "{trivia:?}");
996 assert_eq!(trivia[0].kind, TriviaKind::BlockComment);
997 }
998
999 #[test]
1000 fn blank_line_between_comments_counted() {
1001 let src = "-- a\n\n-- b\nx = 1\n";
1002 let kinds: Vec<_> = trivia_of(src).into_iter().map(|t| t.kind).collect();
1003 assert_eq!(
1004 kinds,
1005 vec![
1006 TriviaKind::LineComment,
1007 TriviaKind::BlankLines(1),
1008 TriviaKind::LineComment,
1009 ]
1010 );
1011 }
1012
1013 #[test]
1014 fn cpp_directive_becomes_trivia() {
1015 let src = "#ifdef DAML_BIGNUMERIC\nx = 1\n#endif\n";
1016 let trivia = trivia_of(src);
1017 assert_eq!(trivia.len(), 2);
1018 assert!(trivia.iter().all(|t| t.kind == TriviaKind::CppDirective));
1019 assert_eq!(trivia[0].text, "#ifdef DAML_BIGNUMERIC");
1020 }
1021
1022 #[test]
1023 fn round_trip_is_byte_identical() {
1024 assert_round_trip("module M where\n\n-- doc\nf : Int -> Int\nf x = x + 1\n");
1025 assert_round_trip("x = \"tem\\\"plate \\n\" {- block {- nested -} -}\r\ny = 'a'\r\n");
1026 assert_round_trip("\tärger = [1..5] -- ütf\n");
1027 assert_round_trip("s = \"gap \\ \\ here\"\n");
1028 assert_round_trip("#ifdef X\nf = 0x1F\n#endif\n");
1029 assert_round_trip("\n\n \nf = 1 \n ");
1030 assert_round_trip("");
1031 }
1032
1033 #[test]
1034 fn render_lossless_detects_lost_bytes() {
1035 let src = "x = 1 -- comment\n";
1036 let (tokens, mut trivia, _) = lex_with_trivia(src);
1037 trivia.clear(); assert!(render_lossless(src, &tokens, &trivia).is_err());
1039 }
1040
1041 #[test]
1042 fn unicode_identifier() {
1043 assert_eq!(
1044 toks("ärger = 1"),
1045 vec![lower("ärger"), Tok::Op("=".into()), Tok::IntLit("1".into())]
1046 );
1047 let _ = upper("Ülf"); }
1049}