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 self.bump();
458 match self.peek() {
459 Some(w) if w.is_whitespace() => {
461 while self.peek().is_some_and(|c| c.is_whitespace()) {
462 self.bump();
463 }
464 if self.peek() == Some('\\') {
465 self.bump();
466 } else {
467 self.error("unterminated string gap", pos);
468 break;
469 }
470 }
471 Some(e) => {
472 self.bump();
473 value.push(unescape(e));
474 }
475 None => {
476 self.error("unterminated string literal", pos);
477 break;
478 }
479 }
480 }
481 Some(c) => {
482 self.bump();
483 value.push(c);
484 }
485 }
486 }
487 self.push(Tok::StringLit(value), pos, start);
488 }
489
490 fn char_lit(&mut self, pos: Pos) {
494 let start = self.byte;
495 let mut j = self.i + 1;
497 let mut escaped = false;
498 let mut ok = false;
499 let window_end = (self.i + 12).min(self.chars.len());
500 while j < window_end {
501 match self.chars[j] {
502 '\\' if !escaped => escaped = true,
503 '\'' if !escaped => {
504 ok = j > self.i + 1;
505 break;
506 }
507 '\n' => break,
508 _ => escaped = false,
509 }
510 j += 1;
511 }
512 if !ok {
513 self.bump();
514 self.error("stray single quote", pos);
515 return;
516 }
517 self.bump(); let mut value = String::new();
519 while self.peek() != Some('\'') {
520 let c = self.bump().unwrap();
521 if c == '\\' {
522 if let Some(e) = self.bump() {
523 value.push(unescape(e));
524 }
525 } else {
526 value.push(c);
527 }
528 }
529 self.bump(); self.push(Tok::CharLit(value), pos, start);
531 }
532
533 fn number(&mut self, pos: Pos) {
534 let start = self.byte;
535 let mut text = String::new();
536 if self.peek() == Some('0') && matches!(self.peek_at(1), Some('x' | 'X')) {
537 text.push(self.bump().unwrap());
538 text.push(self.bump().unwrap());
539 while self
540 .peek()
541 .is_some_and(|c| c.is_ascii_hexdigit() || c == '_')
542 {
543 text.push(self.bump().unwrap());
544 }
545 self.push(Tok::IntLit(text), pos, start);
546 return;
547 }
548 while self.peek().is_some_and(|c| c.is_ascii_digit() || c == '_') {
549 text.push(self.bump().unwrap());
550 }
551 let mut decimal = false;
552 if self.peek() == Some('.') && self.peek_at(1).is_some_and(|c| c.is_ascii_digit()) {
554 decimal = true;
555 text.push(self.bump().unwrap());
556 while self.peek().is_some_and(|c| c.is_ascii_digit() || c == '_') {
557 text.push(self.bump().unwrap());
558 }
559 }
560 if matches!(self.peek(), Some('e' | 'E'))
561 && (self.peek_at(1).is_some_and(|c| c.is_ascii_digit())
562 || (matches!(self.peek_at(1), Some('+' | '-'))
563 && self.peek_at(2).is_some_and(|c| c.is_ascii_digit())))
564 {
565 decimal = true;
566 text.push(self.bump().unwrap());
567 if matches!(self.peek(), Some('+' | '-')) {
568 text.push(self.bump().unwrap());
569 }
570 while self.peek().is_some_and(|c| c.is_ascii_digit()) {
571 text.push(self.bump().unwrap());
572 }
573 }
574 if decimal {
575 self.push(Tok::DecimalLit(text), pos, start);
576 } else {
577 self.push(Tok::IntLit(text), pos, start);
578 }
579 }
580
581 fn identifier(&mut self, pos: Pos) {
584 let start = self.byte;
585 let mut segments: Vec<String> = Vec::new();
586 loop {
587 let mut seg = String::new();
588 while self.peek().is_some_and(is_ident_char) {
589 seg.push(self.bump().unwrap());
590 }
591 let seg_is_upper = seg.chars().next().is_some_and(|c| c.is_uppercase());
592 segments.push(seg);
593 if seg_is_upper
596 && self.peek() == Some('.')
597 && self.peek_at(1).is_some_and(is_ident_start)
598 {
599 self.bump(); continue;
601 }
602 break;
603 }
604 let name = segments.pop().unwrap();
605 let qualifier = if segments.is_empty() {
606 None
607 } else {
608 Some(segments.join("."))
609 };
610 let tok = if name.chars().next().is_some_and(|c| c.is_uppercase()) {
611 Tok::UpperId { qualifier, name }
612 } else {
613 Tok::LowerId { qualifier, name }
614 };
615 self.push(tok, pos, start);
616 }
617
618 fn operator(&mut self, pos: Pos) {
619 let start = self.i;
620 let byte_start = self.byte;
621 while self.peek().is_some_and(is_symbol_char) {
622 self.i += 1;
626 self.column += 1;
627 self.byte += 1;
628 }
629 let text: String = self.chars[start..self.i].iter().collect();
630 if text.len() >= 2 && text.chars().all(|c| c == '-') {
633 while self.peek().is_some_and(|c| c != '\n') {
634 self.bump();
635 }
636 self.push_trivia(TriviaKind::LineComment, pos, byte_start);
637 return;
638 }
639 self.push(Tok::Op(text), pos, byte_start);
640 }
641}
642
643const fn unescape(c: char) -> char {
644 match c {
645 'n' => '\n',
646 't' => '\t',
647 'r' => '\r',
648 '0' => '\0',
649 other => other,
650 }
651}
652
653impl Tok {
654 pub const fn keyword(&self) -> Option<&str> {
657 match self {
658 Self::LowerId {
659 qualifier: None,
660 name,
661 } => Some(name.as_str()),
662 _ => None,
663 }
664 }
665
666 pub fn is_keyword(&self, kw: &str) -> bool {
667 self.keyword() == Some(kw)
668 }
669
670 pub fn is_op(&self, op: &str) -> bool {
671 matches!(self, Self::Op(o) if o == op)
672 }
673}
674
675#[cfg(test)]
676mod tests {
677 use super::*;
678
679 fn toks(src: &str) -> Vec<Tok> {
680 let (tokens, errors) = lex(src);
681 assert!(errors.is_empty(), "lex errors: {errors:?}");
682 tokens.into_iter().map(|t| t.tok).collect()
683 }
684
685 fn lower(name: &str) -> Tok {
686 Tok::LowerId {
687 qualifier: None,
688 name: name.to_string(),
689 }
690 }
691
692 fn upper(name: &str) -> Tok {
693 Tok::UpperId {
694 qualifier: None,
695 name: name.to_string(),
696 }
697 }
698
699 #[test]
700 fn line_comment_with_keywords_produces_no_tokens() {
701 assert_eq!(toks("-- electing to exercise the option"), vec![]);
702 assert_eq!(toks("--- template Foo"), vec![]);
703 }
704
705 #[test]
706 fn arrow_like_operator_is_not_comment() {
707 assert_eq!(
708 toks("a --> b"),
709 vec![lower("a"), Tok::Op("-->".into()), lower("b")]
710 );
711 }
712
713 #[test]
714 fn nested_block_comment() {
715 assert_eq!(toks("{- outer {- inner -} still -} x"), vec![lower("x")]);
716 }
717
718 #[test]
719 fn string_with_keyword_and_escapes() {
720 assert_eq!(
721 toks(r#""template \"Foo\" \n""#),
722 vec![Tok::StringLit("template \"Foo\" \n".into())]
723 );
724 }
725
726 #[test]
727 fn qualified_identifiers() {
728 assert_eq!(
729 toks("DA.Set.fromList Map.Map foo"),
730 vec![
731 Tok::LowerId {
732 qualifier: Some("DA.Set".into()),
733 name: "fromList".into()
734 },
735 Tok::UpperId {
736 qualifier: Some("Map".into()),
737 name: "Map".into()
738 },
739 lower("foo"),
740 ]
741 );
742 }
743
744 #[test]
745 fn numbers() {
746 assert_eq!(
747 toks("42 1.5 0x1F 2e3 1_000"),
748 vec![
749 Tok::IntLit("42".into()),
750 Tok::DecimalLit("1.5".into()),
751 Tok::IntLit("0x1F".into()),
752 Tok::DecimalLit("2e3".into()),
753 Tok::IntLit("1_000".into()),
754 ]
755 );
756 }
757
758 #[test]
759 fn enum_from_to_is_not_decimal() {
760 assert_eq!(
761 toks("[1..5]"),
762 vec![
763 Tok::LBracket,
764 Tok::IntLit("1".into()),
765 Tok::Op("..".into()),
766 Tok::IntLit("5".into()),
767 Tok::RBracket,
768 ]
769 );
770 }
771
772 #[test]
773 fn primes_stay_in_identifier_and_char_lit_works() {
774 assert_eq!(
775 toks(r"foo' 'a' '\n'"),
776 vec![
777 lower("foo'"),
778 Tok::CharLit("a".into()),
779 Tok::CharLit("\n".into())
780 ]
781 );
782 }
783
784 #[test]
785 fn operators_and_punctuation() {
786 assert_eq!(
787 toks("x <- f (y, z) `div` 2"),
788 vec![
789 lower("x"),
790 Tok::Op("<-".into()),
791 lower("f"),
792 Tok::LParen,
793 lower("y"),
794 Tok::Comma,
795 lower("z"),
796 Tok::RParen,
797 Tok::Backtick,
798 lower("div"),
799 Tok::Backtick,
800 Tok::IntLit("2".into()),
801 ]
802 );
803 }
804
805 #[test]
806 fn spans_are_one_based() {
807 let (tokens, _) = lex("ab\n cd");
808 assert_eq!(tokens[0].pos, Pos { line: 1, column: 1 });
809 assert_eq!(tokens[1].pos, Pos { line: 2, column: 3 });
810 }
811
812 #[test]
813 fn tab_advances_to_stop() {
814 let (tokens, _) = lex("\tx");
815 assert_eq!(tokens[0].pos, Pos { line: 1, column: 9 });
816 }
817
818 #[test]
819 fn unterminated_string_is_error_not_hang() {
820 let (_, errors) = lex("x = \"oops\ny");
821 assert_eq!(errors.len(), 1);
822 }
823
824 #[test]
825 fn unterminated_block_comment_is_error_not_hang() {
826 let (_, errors) = lex("{- never closed");
827 assert_eq!(errors.len(), 1);
828 }
829
830 fn trivia_of(src: &str) -> Vec<Trivia> {
831 let (_, trivia, _) = lex_with_trivia(src);
832 trivia
833 }
834
835 fn assert_round_trip(src: &str) {
838 let (tokens, trivia, errors) = lex_with_trivia(src);
839 assert!(errors.is_empty(), "lex errors: {errors:?}");
840 assert_eq!(
841 render_lossless(src, &tokens, &trivia).as_deref(),
842 Ok(src),
843 "round trip failed for {src:?}"
844 );
845 }
846
847 #[test]
848 fn line_comment_becomes_trivia_with_exact_text_and_span() {
849 let src = "x = 1 -- electing to exercise\ny = 2\n";
850 let trivia = trivia_of(src);
851 assert_eq!(trivia.len(), 1);
852 assert_eq!(trivia[0].kind, TriviaKind::LineComment);
853 assert_eq!(trivia[0].text, "-- electing to exercise");
854 assert_eq!(&src[trivia[0].start..trivia[0].end], trivia[0].text);
855 assert_eq!(trivia[0].pos, Pos { line: 1, column: 7 });
856 }
857
858 #[test]
859 fn nested_block_comment_becomes_one_trivia() {
860 let src = "{- outer {- inner -} still -} x";
861 let trivia = trivia_of(src);
862 assert_eq!(trivia.len(), 1);
863 assert_eq!(trivia[0].kind, TriviaKind::BlockComment);
864 assert_eq!(trivia[0].text, "{- outer {- inner -} still -}");
865 }
866
867 #[test]
868 fn unterminated_block_comment_still_yields_trivia_to_eof() {
869 let (_, trivia, errors) = lex_with_trivia("x {- never closed");
870 assert_eq!(errors.len(), 1);
871 assert_eq!(trivia.len(), 1);
872 assert_eq!(trivia[0].text, "{- never closed");
873 }
874
875 #[test]
876 fn blank_lines_between_items_counted() {
877 let src = "x = 1\n\n\ny = 2\n";
878 let trivia = trivia_of(src);
879 assert_eq!(trivia.len(), 1);
880 assert_eq!(trivia[0].kind, TriviaKind::BlankLines(2));
881 assert_eq!(trivia[0].pos, Pos { line: 2, column: 1 });
882 }
883
884 #[test]
885 fn blank_line_at_file_start_counted() {
886 let trivia = trivia_of("\nx = 1\n");
887 assert_eq!(trivia.len(), 1);
888 assert_eq!(trivia[0].kind, TriviaKind::BlankLines(1));
889 assert_eq!(trivia[0].pos, Pos { line: 1, column: 1 });
890 }
891
892 #[test]
893 fn blank_looking_lines_inside_block_comment_are_not_blank_trivia() {
894 let src = "x = 1 {- a\n\nb -}\ny = 2\n";
895 let trivia = trivia_of(src);
896 assert_eq!(trivia.len(), 1, "{trivia:?}");
897 assert_eq!(trivia[0].kind, TriviaKind::BlockComment);
898 }
899
900 #[test]
901 fn blank_line_between_comments_counted() {
902 let src = "-- a\n\n-- b\nx = 1\n";
903 let kinds: Vec<_> = trivia_of(src).into_iter().map(|t| t.kind).collect();
904 assert_eq!(
905 kinds,
906 vec![
907 TriviaKind::LineComment,
908 TriviaKind::BlankLines(1),
909 TriviaKind::LineComment,
910 ]
911 );
912 }
913
914 #[test]
915 fn cpp_directive_becomes_trivia() {
916 let src = "#ifdef DAML_BIGNUMERIC\nx = 1\n#endif\n";
917 let trivia = trivia_of(src);
918 assert_eq!(trivia.len(), 2);
919 assert!(trivia.iter().all(|t| t.kind == TriviaKind::CppDirective));
920 assert_eq!(trivia[0].text, "#ifdef DAML_BIGNUMERIC");
921 }
922
923 #[test]
924 fn round_trip_is_byte_identical() {
925 assert_round_trip("module M where\n\n-- doc\nf : Int -> Int\nf x = x + 1\n");
926 assert_round_trip("x = \"tem\\\"plate \\n\" {- block {- nested -} -}\r\ny = 'a'\r\n");
927 assert_round_trip("\tärger = [1..5] -- ütf\n");
928 assert_round_trip("s = \"gap \\ \\ here\"\n");
929 assert_round_trip("#ifdef X\nf = 0x1F\n#endif\n");
930 assert_round_trip("\n\n \nf = 1 \n ");
931 assert_round_trip("");
932 }
933
934 #[test]
935 fn render_lossless_detects_lost_bytes() {
936 let src = "x = 1 -- comment\n";
937 let (tokens, mut trivia, _) = lex_with_trivia(src);
938 trivia.clear(); assert!(render_lossless(src, &tokens, &trivia).is_err());
940 }
941
942 #[test]
943 fn unicode_identifier() {
944 assert_eq!(
945 toks("ärger = 1"),
946 vec![lower("ärger"), Tok::Op("=".into()), Tok::IntLit("1".into())]
947 );
948 let _ = upper("Ülf"); }
950}