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 {}..{} lost (not covered by any token/trivia): {:?}",
207 prev, start, gap
208 ));
209 }
210 out.push_str(gap);
211 out.push_str(&source[start..end]);
212 prev = end;
213 }
214 let tail = &source[prev..];
215 if !tail.chars().all(char::is_whitespace) {
216 return Err(format!("bytes {}.. lost at EOF: {:?}", prev, tail));
217 }
218 out.push_str(tail);
219 Ok(out)
220}
221
222fn add_blank_line_trivia(source: &str, tokens: &[Token], trivia: &mut Vec<Trivia>) {
227 let mut spans: Vec<(usize, usize)> = tokens
228 .iter()
229 .map(|t| (t.start, t.end))
230 .chain(trivia.iter().map(|t| (t.start, t.end)))
231 .collect();
232 spans.sort_unstable();
233 let bytes = source.as_bytes();
234 let mut blanks = Vec::new();
235 let mut gap_start = 0usize;
236 let emit_gap = |from: usize, to: usize, out: &mut Vec<Trivia>| {
237 let newlines: Vec<usize> = (from..to).filter(|&i| bytes[i] == b'\n').collect();
242 let count = if from == 0 {
246 newlines.len()
247 } else {
248 newlines.len().saturating_sub(1)
249 };
250 if count == 0 {
251 return;
252 }
253 let region_start = if from == 0 { 0 } else { newlines[0] + 1 };
254 let region_end = newlines[newlines.len() - 1] + 1;
255 let line = source[..region_start].matches('\n').count() + 1;
256 out.push(Trivia {
257 kind: TriviaKind::BlankLines(count),
258 text: String::new(),
259 pos: Pos { line, column: 1 },
260 start: region_start,
261 end: region_end,
262 });
263 };
264 for &(s, e) in &spans {
265 if s > gap_start {
266 emit_gap(gap_start, s, &mut blanks);
267 }
268 gap_start = gap_start.max(e);
269 }
270 if source.len() > gap_start {
271 emit_gap(gap_start, source.len(), &mut blanks);
272 }
273 trivia.extend(blanks);
274}
275
276impl<'a> Lexer<'a> {
277 fn peek(&self) -> Option<char> {
278 self.chars.get(self.i).copied()
279 }
280
281 fn peek_at(&self, n: usize) -> Option<char> {
282 self.chars.get(self.i + n).copied()
283 }
284
285 fn bump(&mut self) -> Option<char> {
286 let c = self.chars.get(self.i).copied()?;
287 self.i += 1;
288 self.byte += c.len_utf8();
289 match c {
290 '\n' => {
291 self.line += 1;
292 self.column = 1;
293 }
294 '\t' => {
295 self.column = ((self.column - 1) / TAB_STOP + 1) * TAB_STOP + 1;
298 }
299 _ => self.column += 1,
300 }
301 Some(c)
302 }
303
304 const fn pos(&self) -> Pos {
305 Pos {
306 line: self.line,
307 column: self.column,
308 }
309 }
310
311 fn push(&mut self, tok: Tok, pos: Pos, start: usize) {
314 self.tokens.push(Token {
315 tok,
316 pos,
317 start,
318 end: self.byte,
319 });
320 }
321
322 fn push_trivia(&mut self, kind: TriviaKind, pos: Pos, start: usize) {
323 self.trivia.push(Trivia {
324 kind,
325 text: self.src[start..self.byte].to_string(),
326 pos,
327 start,
328 end: self.byte,
329 });
330 }
331
332 fn error(&mut self, message: impl Into<String>, pos: Pos) {
333 self.errors.push(LexError {
334 message: message.into(),
335 pos,
336 });
337 }
338
339 fn scan_tokens(&mut self) {
340 while let Some(c) = self.peek() {
341 let pos = self.pos();
342 let start = self.byte;
343 match c {
344 ' ' | '\t' | '\n' | '\r' => {
345 self.bump();
346 }
347 '(' => {
348 self.bump();
349 self.push(Tok::LParen, pos, start);
350 }
351 ')' => {
352 self.bump();
353 self.push(Tok::RParen, pos, start);
354 }
355 '[' => {
356 self.bump();
357 self.push(Tok::LBracket, pos, start);
358 }
359 ']' => {
360 self.bump();
361 self.push(Tok::RBracket, pos, start);
362 }
363 ',' => {
364 self.bump();
365 self.push(Tok::Comma, pos, start);
366 }
367 ';' => {
368 self.bump();
369 self.push(Tok::Semi, pos, start);
370 }
371 '`' => {
372 self.bump();
373 self.push(Tok::Backtick, pos, start);
374 }
375 '{' => {
376 if self.peek_at(1) == Some('-') {
377 self.block_comment(pos);
378 } else {
379 self.bump();
380 self.push(Tok::LBrace, pos, start);
381 }
382 }
383 '}' => {
384 self.bump();
385 self.push(Tok::RBrace, pos, start);
386 }
387 '#' if self.column == 1
391 && self.peek_at(1).is_some_and(|c| c.is_ascii_lowercase()) =>
392 {
393 while self.peek().is_some_and(|c| c != '\n') {
394 self.bump();
395 }
396 self.push_trivia(TriviaKind::CppDirective, pos, start);
397 }
398 '"' => self.string_lit(pos),
399 '\'' => self.char_lit(pos),
400 c if c.is_ascii_digit() => self.number(pos),
401 c if is_ident_start(c) => self.identifier(pos),
402 c if is_symbol_char(c) => self.operator(pos),
403 _ => {
404 self.bump();
405 self.error(format!("unexpected character '{}'", c), pos);
406 }
407 }
408 }
409 }
410
411 fn block_comment(&mut self, pos: Pos) {
414 let start = self.byte;
415 self.bump(); self.bump(); let mut depth = 1usize;
418 while depth > 0 {
419 match self.peek() {
420 None => {
421 self.error("unterminated block comment", pos);
422 self.push_trivia(TriviaKind::BlockComment, pos, start);
423 return;
424 }
425 Some('{') if self.peek_at(1) == Some('-') => {
426 self.bump();
427 self.bump();
428 depth += 1;
429 }
430 Some('-') if self.peek_at(1) == Some('}') => {
431 self.bump();
432 self.bump();
433 depth -= 1;
434 }
435 Some(_) => {
436 self.bump();
437 }
438 }
439 }
440 self.push_trivia(TriviaKind::BlockComment, pos, start);
441 }
442
443 fn string_lit(&mut self, pos: Pos) {
444 let start = self.byte;
445 self.bump(); let mut value = String::new();
447 loop {
448 match self.peek() {
449 None | Some('\n') => {
450 self.error("unterminated string literal", pos);
451 break;
452 }
453 Some('"') => {
454 self.bump();
455 break;
456 }
457 Some('\\') => {
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 value.push(unescape(e));
475 }
476 None => {
477 self.error("unterminated string literal", pos);
478 break;
479 }
480 }
481 }
482 Some(c) => {
483 self.bump();
484 value.push(c);
485 }
486 }
487 }
488 self.push(Tok::StringLit(value), pos, start);
489 }
490
491 fn char_lit(&mut self, pos: Pos) {
495 let start = self.byte;
496 let mut j = self.i + 1;
498 let mut escaped = false;
499 let mut ok = false;
500 let window_end = (self.i + 12).min(self.chars.len());
501 while j < window_end {
502 match self.chars[j] {
503 '\\' if !escaped => escaped = true,
504 '\'' if !escaped => {
505 ok = j > self.i + 1;
506 break;
507 }
508 '\n' => break,
509 _ => escaped = false,
510 }
511 j += 1;
512 }
513 if !ok {
514 self.bump();
515 self.error("stray single quote", pos);
516 return;
517 }
518 self.bump(); let mut value = String::new();
520 while self.peek() != Some('\'') {
521 let c = self.bump().unwrap();
522 if c == '\\' {
523 if let Some(e) = self.bump() {
524 value.push(unescape(e));
525 }
526 } else {
527 value.push(c);
528 }
529 }
530 self.bump(); self.push(Tok::CharLit(value), pos, start);
532 }
533
534 fn number(&mut self, pos: Pos) {
535 let start = self.byte;
536 let mut text = String::new();
537 if self.peek() == Some('0') && matches!(self.peek_at(1), Some('x') | Some('X')) {
538 text.push(self.bump().unwrap());
539 text.push(self.bump().unwrap());
540 while self
541 .peek()
542 .is_some_and(|c| c.is_ascii_hexdigit() || c == '_')
543 {
544 text.push(self.bump().unwrap());
545 }
546 self.push(Tok::IntLit(text), pos, start);
547 return;
548 }
549 while self.peek().is_some_and(|c| c.is_ascii_digit() || c == '_') {
550 text.push(self.bump().unwrap());
551 }
552 let mut decimal = false;
553 if self.peek() == Some('.') && self.peek_at(1).is_some_and(|c| c.is_ascii_digit()) {
555 decimal = true;
556 text.push(self.bump().unwrap());
557 while self.peek().is_some_and(|c| c.is_ascii_digit() || c == '_') {
558 text.push(self.bump().unwrap());
559 }
560 }
561 if matches!(self.peek(), Some('e') | Some('E'))
562 && (self.peek_at(1).is_some_and(|c| c.is_ascii_digit())
563 || (matches!(self.peek_at(1), Some('+') | Some('-'))
564 && self.peek_at(2).is_some_and(|c| c.is_ascii_digit())))
565 {
566 decimal = true;
567 text.push(self.bump().unwrap());
568 if matches!(self.peek(), Some('+') | Some('-')) {
569 text.push(self.bump().unwrap());
570 }
571 while self.peek().is_some_and(|c| c.is_ascii_digit()) {
572 text.push(self.bump().unwrap());
573 }
574 }
575 if decimal {
576 self.push(Tok::DecimalLit(text), pos, start);
577 } else {
578 self.push(Tok::IntLit(text), pos, start);
579 }
580 }
581
582 fn identifier(&mut self, pos: Pos) {
585 let start = self.byte;
586 let mut segments: Vec<String> = Vec::new();
587 loop {
588 let mut seg = String::new();
589 while self.peek().is_some_and(is_ident_char) {
590 seg.push(self.bump().unwrap());
591 }
592 let seg_is_upper = seg.chars().next().is_some_and(|c| c.is_uppercase());
593 segments.push(seg);
594 if seg_is_upper
597 && self.peek() == Some('.')
598 && self.peek_at(1).is_some_and(is_ident_start)
599 {
600 self.bump(); continue;
602 }
603 break;
604 }
605 let name = segments.pop().unwrap();
606 let qualifier = if segments.is_empty() {
607 None
608 } else {
609 Some(segments.join("."))
610 };
611 let tok = if name.chars().next().is_some_and(|c| c.is_uppercase()) {
612 Tok::UpperId { qualifier, name }
613 } else {
614 Tok::LowerId { qualifier, name }
615 };
616 self.push(tok, pos, start);
617 }
618
619 fn operator(&mut self, pos: Pos) {
620 let start = self.i;
621 let byte_start = self.byte;
622 while self.peek().is_some_and(is_symbol_char) {
623 self.i += 1;
627 self.column += 1;
628 self.byte += 1;
629 }
630 let text: String = self.chars[start..self.i].iter().collect();
631 if text.len() >= 2 && text.chars().all(|c| c == '-') {
634 while self.peek().is_some_and(|c| c != '\n') {
635 self.bump();
636 }
637 self.push_trivia(TriviaKind::LineComment, pos, byte_start);
638 return;
639 }
640 self.push(Tok::Op(text), pos, byte_start);
641 }
642}
643
644const fn unescape(c: char) -> char {
645 match c {
646 'n' => '\n',
647 't' => '\t',
648 'r' => '\r',
649 '0' => '\0',
650 other => other,
651 }
652}
653
654impl Tok {
655 pub const fn keyword(&self) -> Option<&str> {
658 match self {
659 Self::LowerId {
660 qualifier: None,
661 name,
662 } => Some(name.as_str()),
663 _ => None,
664 }
665 }
666
667 pub fn is_keyword(&self, kw: &str) -> bool {
668 self.keyword() == Some(kw)
669 }
670
671 pub fn is_op(&self, op: &str) -> bool {
672 matches!(self, Self::Op(o) if o == op)
673 }
674}
675
676#[cfg(test)]
677mod tests {
678 use super::*;
679
680 fn toks(src: &str) -> Vec<Tok> {
681 let (tokens, errors) = lex(src);
682 assert!(errors.is_empty(), "lex errors: {:?}", errors);
683 tokens.into_iter().map(|t| t.tok).collect()
684 }
685
686 fn lower(name: &str) -> Tok {
687 Tok::LowerId {
688 qualifier: None,
689 name: name.to_string(),
690 }
691 }
692
693 fn upper(name: &str) -> Tok {
694 Tok::UpperId {
695 qualifier: None,
696 name: name.to_string(),
697 }
698 }
699
700 #[test]
701 fn line_comment_with_keywords_produces_no_tokens() {
702 assert_eq!(toks("-- electing to exercise the option"), vec![]);
703 assert_eq!(toks("--- template Foo"), vec![]);
704 }
705
706 #[test]
707 fn arrow_like_operator_is_not_comment() {
708 assert_eq!(
709 toks("a --> b"),
710 vec![lower("a"), Tok::Op("-->".into()), lower("b")]
711 );
712 }
713
714 #[test]
715 fn nested_block_comment() {
716 assert_eq!(toks("{- outer {- inner -} still -} x"), vec![lower("x")]);
717 }
718
719 #[test]
720 fn string_with_keyword_and_escapes() {
721 assert_eq!(
722 toks(r#""template \"Foo\" \n""#),
723 vec![Tok::StringLit("template \"Foo\" \n".into())]
724 );
725 }
726
727 #[test]
728 fn qualified_identifiers() {
729 assert_eq!(
730 toks("DA.Set.fromList Map.Map foo"),
731 vec![
732 Tok::LowerId {
733 qualifier: Some("DA.Set".into()),
734 name: "fromList".into()
735 },
736 Tok::UpperId {
737 qualifier: Some("Map".into()),
738 name: "Map".into()
739 },
740 lower("foo"),
741 ]
742 );
743 }
744
745 #[test]
746 fn numbers() {
747 assert_eq!(
748 toks("42 1.5 0x1F 2e3 1_000"),
749 vec![
750 Tok::IntLit("42".into()),
751 Tok::DecimalLit("1.5".into()),
752 Tok::IntLit("0x1F".into()),
753 Tok::DecimalLit("2e3".into()),
754 Tok::IntLit("1_000".into()),
755 ]
756 );
757 }
758
759 #[test]
760 fn enum_from_to_is_not_decimal() {
761 assert_eq!(
762 toks("[1..5]"),
763 vec![
764 Tok::LBracket,
765 Tok::IntLit("1".into()),
766 Tok::Op("..".into()),
767 Tok::IntLit("5".into()),
768 Tok::RBracket,
769 ]
770 );
771 }
772
773 #[test]
774 fn primes_stay_in_identifier_and_char_lit_works() {
775 assert_eq!(
776 toks(r"foo' 'a' '\n'"),
777 vec![
778 lower("foo'"),
779 Tok::CharLit("a".into()),
780 Tok::CharLit("\n".into())
781 ]
782 );
783 }
784
785 #[test]
786 fn operators_and_punctuation() {
787 assert_eq!(
788 toks("x <- f (y, z) `div` 2"),
789 vec![
790 lower("x"),
791 Tok::Op("<-".into()),
792 lower("f"),
793 Tok::LParen,
794 lower("y"),
795 Tok::Comma,
796 lower("z"),
797 Tok::RParen,
798 Tok::Backtick,
799 lower("div"),
800 Tok::Backtick,
801 Tok::IntLit("2".into()),
802 ]
803 );
804 }
805
806 #[test]
807 fn spans_are_one_based() {
808 let (tokens, _) = lex("ab\n cd");
809 assert_eq!(tokens[0].pos, Pos { line: 1, column: 1 });
810 assert_eq!(tokens[1].pos, Pos { line: 2, column: 3 });
811 }
812
813 #[test]
814 fn tab_advances_to_stop() {
815 let (tokens, _) = lex("\tx");
816 assert_eq!(tokens[0].pos, Pos { line: 1, column: 9 });
817 }
818
819 #[test]
820 fn unterminated_string_is_error_not_hang() {
821 let (_, errors) = lex("x = \"oops\ny");
822 assert_eq!(errors.len(), 1);
823 }
824
825 #[test]
826 fn unterminated_block_comment_is_error_not_hang() {
827 let (_, errors) = lex("{- never closed");
828 assert_eq!(errors.len(), 1);
829 }
830
831 fn trivia_of(src: &str) -> Vec<Trivia> {
832 let (_, trivia, _) = lex_with_trivia(src);
833 trivia
834 }
835
836 fn assert_round_trip(src: &str) {
839 let (tokens, trivia, errors) = lex_with_trivia(src);
840 assert!(errors.is_empty(), "lex errors: {:?}", errors);
841 assert_eq!(
842 render_lossless(src, &tokens, &trivia).as_deref(),
843 Ok(src),
844 "round trip failed for {:?}",
845 src
846 );
847 }
848
849 #[test]
850 fn line_comment_becomes_trivia_with_exact_text_and_span() {
851 let src = "x = 1 -- electing to exercise\ny = 2\n";
852 let trivia = trivia_of(src);
853 assert_eq!(trivia.len(), 1);
854 assert_eq!(trivia[0].kind, TriviaKind::LineComment);
855 assert_eq!(trivia[0].text, "-- electing to exercise");
856 assert_eq!(&src[trivia[0].start..trivia[0].end], trivia[0].text);
857 assert_eq!(trivia[0].pos, Pos { line: 1, column: 7 });
858 }
859
860 #[test]
861 fn nested_block_comment_becomes_one_trivia() {
862 let src = "{- outer {- inner -} still -} x";
863 let trivia = trivia_of(src);
864 assert_eq!(trivia.len(), 1);
865 assert_eq!(trivia[0].kind, TriviaKind::BlockComment);
866 assert_eq!(trivia[0].text, "{- outer {- inner -} still -}");
867 }
868
869 #[test]
870 fn unterminated_block_comment_still_yields_trivia_to_eof() {
871 let (_, trivia, errors) = lex_with_trivia("x {- never closed");
872 assert_eq!(errors.len(), 1);
873 assert_eq!(trivia.len(), 1);
874 assert_eq!(trivia[0].text, "{- never closed");
875 }
876
877 #[test]
878 fn blank_lines_between_items_counted() {
879 let src = "x = 1\n\n\ny = 2\n";
880 let trivia = trivia_of(src);
881 assert_eq!(trivia.len(), 1);
882 assert_eq!(trivia[0].kind, TriviaKind::BlankLines(2));
883 assert_eq!(trivia[0].pos, Pos { line: 2, column: 1 });
884 }
885
886 #[test]
887 fn blank_line_at_file_start_counted() {
888 let trivia = trivia_of("\nx = 1\n");
889 assert_eq!(trivia.len(), 1);
890 assert_eq!(trivia[0].kind, TriviaKind::BlankLines(1));
891 assert_eq!(trivia[0].pos, Pos { line: 1, column: 1 });
892 }
893
894 #[test]
895 fn blank_looking_lines_inside_block_comment_are_not_blank_trivia() {
896 let src = "x = 1 {- a\n\nb -}\ny = 2\n";
897 let trivia = trivia_of(src);
898 assert_eq!(trivia.len(), 1, "{:?}", trivia);
899 assert_eq!(trivia[0].kind, TriviaKind::BlockComment);
900 }
901
902 #[test]
903 fn blank_line_between_comments_counted() {
904 let src = "-- a\n\n-- b\nx = 1\n";
905 let kinds: Vec<_> = trivia_of(src).into_iter().map(|t| t.kind).collect();
906 assert_eq!(
907 kinds,
908 vec![
909 TriviaKind::LineComment,
910 TriviaKind::BlankLines(1),
911 TriviaKind::LineComment,
912 ]
913 );
914 }
915
916 #[test]
917 fn cpp_directive_becomes_trivia() {
918 let src = "#ifdef DAML_BIGNUMERIC\nx = 1\n#endif\n";
919 let trivia = trivia_of(src);
920 assert_eq!(trivia.len(), 2);
921 assert!(trivia.iter().all(|t| t.kind == TriviaKind::CppDirective));
922 assert_eq!(trivia[0].text, "#ifdef DAML_BIGNUMERIC");
923 }
924
925 #[test]
926 fn round_trip_is_byte_identical() {
927 assert_round_trip("module M where\n\n-- doc\nf : Int -> Int\nf x = x + 1\n");
928 assert_round_trip("x = \"tem\\\"plate \\n\" {- block {- nested -} -}\r\ny = 'a'\r\n");
929 assert_round_trip("\tärger = [1..5] -- ütf\n");
930 assert_round_trip("s = \"gap \\ \\ here\"\n");
931 assert_round_trip("#ifdef X\nf = 0x1F\n#endif\n");
932 assert_round_trip("\n\n \nf = 1 \n ");
933 assert_round_trip("");
934 }
935
936 #[test]
937 fn render_lossless_detects_lost_bytes() {
938 let src = "x = 1 -- comment\n";
939 let (tokens, mut trivia, _) = lex_with_trivia(src);
940 trivia.clear(); assert!(render_lossless(src, &tokens, &trivia).is_err());
942 }
943
944 #[test]
945 fn unicode_identifier() {
946 assert_eq!(
947 toks("ärger = 1"),
948 vec![lower("ärger"), Tok::Op("=".into()), Tok::IntLit("1".into())]
949 );
950 let _ = upper("Ülf"); }
952}