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(); self.push(Tok::CharLit(value), pos, start);
551 }
552
553 fn number(&mut self, pos: Pos) {
554 let start = self.byte;
555 let mut text = String::new();
556 if self.peek() == Some('0') && matches!(self.peek_at(1), Some('x' | 'X')) {
557 text.push(self.bump().unwrap());
558 text.push(self.bump().unwrap());
559 let mut has_hex_digit = false;
560 while self
561 .peek()
562 .is_some_and(|c| c.is_ascii_hexdigit() || c == '_')
563 {
564 let c = self.bump().unwrap();
565 has_hex_digit |= c.is_ascii_hexdigit();
566 text.push(c);
567 }
568 if !has_hex_digit {
569 self.error("hex literal requires at least one digit", pos);
570 }
571 self.push(Tok::IntLit(text), pos, start);
572 return;
573 }
574 while self.peek().is_some_and(|c| c.is_ascii_digit() || c == '_') {
575 text.push(self.bump().unwrap());
576 }
577 let mut decimal = false;
578 if self.peek() == Some('.') && self.peek_at(1).is_some_and(|c| c.is_ascii_digit()) {
580 decimal = true;
581 text.push(self.bump().unwrap());
582 while self.peek().is_some_and(|c| c.is_ascii_digit() || c == '_') {
583 text.push(self.bump().unwrap());
584 }
585 }
586 if matches!(self.peek(), Some('e' | 'E')) {
587 decimal = true;
588 if self.peek_at(1).is_some_and(|c| c.is_ascii_digit())
589 || (matches!(self.peek_at(1), Some('+' | '-'))
590 && self.peek_at(2).is_some_and(|c| c.is_ascii_digit()))
591 {
592 text.push(self.bump().unwrap());
593 if matches!(self.peek(), Some('+' | '-')) {
594 text.push(self.bump().unwrap());
595 }
596 while self.peek().is_some_and(|c| c.is_ascii_digit()) {
597 text.push(self.bump().unwrap());
598 }
599 } else {
600 text.push(self.bump().unwrap());
601 if matches!(self.peek(), Some('+' | '-')) {
602 text.push(self.bump().unwrap());
603 }
604 self.error("decimal exponent requires at least one digit", pos);
605 }
606 }
607 if decimal {
608 self.push(Tok::DecimalLit(text), pos, start);
609 } else {
610 self.push(Tok::IntLit(text), pos, start);
611 }
612 }
613
614 fn identifier(&mut self, pos: Pos) {
617 let start = self.byte;
618 let mut segments: Vec<String> = Vec::new();
619 loop {
620 let mut seg = String::new();
621 while self.peek().is_some_and(is_ident_char) {
622 seg.push(self.bump().unwrap());
623 }
624 let seg_is_upper = seg.chars().next().is_some_and(|c| c.is_uppercase());
625 segments.push(seg);
626 if seg_is_upper
629 && self.peek() == Some('.')
630 && self.peek_at(1).is_some_and(is_ident_start)
631 {
632 self.bump(); continue;
634 }
635 break;
636 }
637 let name = segments.pop().unwrap();
638 let qualifier = if segments.is_empty() {
639 None
640 } else {
641 Some(segments.join("."))
642 };
643 let tok = if name.chars().next().is_some_and(|c| c.is_uppercase()) {
644 Tok::UpperId { qualifier, name }
645 } else {
646 Tok::LowerId { qualifier, name }
647 };
648 self.push(tok, pos, start);
649 }
650
651 fn operator(&mut self, pos: Pos) {
652 let start = self.i;
653 let byte_start = self.byte;
654 while self.peek().is_some_and(is_symbol_char) {
655 self.bump();
658 }
659 let text: String = self.chars[start..self.i].iter().collect();
660 if text.len() >= 2 && text.chars().all(|c| c == '-') {
663 while self.peek().is_some_and(|c| c != '\n') {
664 self.bump();
665 }
666 self.push_trivia(TriviaKind::LineComment, pos, byte_start);
667 return;
668 }
669 self.push(Tok::Op(text), pos, byte_start);
670 }
671}
672
673const fn unescape(c: char) -> Option<char> {
674 match c {
675 'n' => Some('\n'),
676 't' => Some('\t'),
677 'r' => Some('\r'),
678 '0' => Some('\0'),
679 'a' => Some('\u{07}'),
680 'b' => Some('\u{08}'),
681 'f' => Some('\u{0c}'),
682 'v' => Some('\u{0b}'),
683 '"' => Some('"'),
684 '\'' => Some('\''),
685 '\\' => Some('\\'),
686 '&' => Some('&'),
687 '1'..='9' | 'o' | 'x' | 'A'..='Z' => Some(c),
693 _ => None,
694 }
695}
696
697impl Tok {
698 pub const fn keyword(&self) -> Option<&str> {
701 match self {
702 Self::LowerId {
703 qualifier: None,
704 name,
705 } => Some(name.as_str()),
706 _ => None,
707 }
708 }
709
710 pub fn is_keyword(&self, kw: &str) -> bool {
711 self.keyword() == Some(kw)
712 }
713
714 pub fn is_op(&self, op: &str) -> bool {
715 matches!(self, Self::Op(o) if o == op)
716 }
717}
718
719#[cfg(test)]
720mod tests {
721 use super::*;
722
723 fn toks(src: &str) -> Vec<Tok> {
724 let (tokens, errors) = lex(src);
725 assert!(errors.is_empty(), "lex errors: {errors:?}");
726 tokens.into_iter().map(|t| t.tok).collect()
727 }
728
729 fn lex_error_messages(src: &str) -> Vec<String> {
730 let (_, errors) = lex(src);
731 errors.into_iter().map(|e| e.message).collect()
732 }
733
734 fn lower(name: &str) -> Tok {
735 Tok::LowerId {
736 qualifier: None,
737 name: name.to_string(),
738 }
739 }
740
741 fn upper(name: &str) -> Tok {
742 Tok::UpperId {
743 qualifier: None,
744 name: name.to_string(),
745 }
746 }
747
748 #[test]
749 fn line_comment_with_keywords_produces_no_tokens() {
750 assert_eq!(toks("-- electing to exercise the option"), vec![]);
751 assert_eq!(toks("--- template Foo"), vec![]);
752 }
753
754 #[test]
755 fn arrow_like_operator_is_not_comment() {
756 assert_eq!(
757 toks("a --> b"),
758 vec![lower("a"), Tok::Op("-->".into()), lower("b")]
759 );
760 }
761
762 #[test]
763 fn nested_block_comment() {
764 assert_eq!(toks("{- outer {- inner -} still -} x"), vec![lower("x")]);
765 }
766
767 #[test]
768 fn string_with_keyword_and_escapes() {
769 assert_eq!(
770 toks(r#""template \"Foo\" \n""#),
771 vec![Tok::StringLit("template \"Foo\" \n".into())]
772 );
773 }
774
775 #[test]
776 fn qualified_identifiers() {
777 assert_eq!(
778 toks("DA.Set.fromList Map.Map foo"),
779 vec![
780 Tok::LowerId {
781 qualifier: Some("DA.Set".into()),
782 name: "fromList".into()
783 },
784 Tok::UpperId {
785 qualifier: Some("Map".into()),
786 name: "Map".into()
787 },
788 lower("foo"),
789 ]
790 );
791 }
792
793 #[test]
794 fn numbers() {
795 assert_eq!(
796 toks("42 1.5 0x1F 2e3 1_000"),
797 vec![
798 Tok::IntLit("42".into()),
799 Tok::DecimalLit("1.5".into()),
800 Tok::IntLit("0x1F".into()),
801 Tok::DecimalLit("2e3".into()),
802 Tok::IntLit("1_000".into()),
803 ]
804 );
805 }
806
807 #[test]
808 fn malformed_hex_literal_reports_error() {
809 assert_eq!(
810 lex_error_messages("0x 0x_"),
811 vec![
812 "hex literal requires at least one digit",
813 "hex literal requires at least one digit",
814 ]
815 );
816 }
817
818 #[test]
819 fn malformed_decimal_exponent_reports_error() {
820 assert_eq!(
821 lex_error_messages("1e 1e+ 1e-"),
822 vec![
823 "decimal exponent requires at least one digit",
824 "decimal exponent requires at least one digit",
825 "decimal exponent requires at least one digit",
826 ]
827 );
828 }
829
830 #[test]
831 fn enum_from_to_is_not_decimal() {
832 assert_eq!(
833 toks("[1..5]"),
834 vec![
835 Tok::LBracket,
836 Tok::IntLit("1".into()),
837 Tok::Op("..".into()),
838 Tok::IntLit("5".into()),
839 Tok::RBracket,
840 ]
841 );
842 }
843
844 #[test]
845 fn primes_stay_in_identifier_and_char_lit_works() {
846 assert_eq!(
847 toks(r"foo' 'a' '\n'"),
848 vec![
849 lower("foo'"),
850 Tok::CharLit("a".into()),
851 Tok::CharLit("\n".into())
852 ]
853 );
854 }
855
856 #[test]
857 fn invalid_escape_sequences_report_errors() {
858 assert_eq!(
859 lex_error_messages(r#""\q" '\q'"#),
860 vec!["invalid escape sequence \\q", "invalid escape sequence \\q"]
861 );
862 }
863
864 #[test]
865 fn operators_and_punctuation() {
866 assert_eq!(
867 toks("x <- f (y, z) `div` 2"),
868 vec![
869 lower("x"),
870 Tok::Op("<-".into()),
871 lower("f"),
872 Tok::LParen,
873 lower("y"),
874 Tok::Comma,
875 lower("z"),
876 Tok::RParen,
877 Tok::Backtick,
878 lower("div"),
879 Tok::Backtick,
880 Tok::IntLit("2".into()),
881 ]
882 );
883 }
884
885 #[test]
886 fn spans_are_one_based() {
887 let (tokens, _) = lex("ab\n cd");
888 assert_eq!(tokens[0].pos, Pos { line: 1, column: 1 });
889 assert_eq!(tokens[1].pos, Pos { line: 2, column: 3 });
890 }
891
892 #[test]
893 fn tab_advances_to_stop() {
894 let (tokens, _) = lex("\tx");
895 assert_eq!(tokens[0].pos, Pos { line: 1, column: 9 });
896 }
897
898 #[test]
899 fn unterminated_string_is_error_not_hang() {
900 let (_, errors) = lex("x = \"oops\ny");
901 assert_eq!(errors.len(), 1);
902 }
903
904 #[test]
905 fn unterminated_block_comment_is_error_not_hang() {
906 let (_, errors) = lex("{- never closed");
907 assert_eq!(errors.len(), 1);
908 }
909
910 fn trivia_of(src: &str) -> Vec<Trivia> {
911 let (_, trivia, _) = lex_with_trivia(src);
912 trivia
913 }
914
915 fn assert_round_trip(src: &str) {
918 let (tokens, trivia, errors) = lex_with_trivia(src);
919 assert!(errors.is_empty(), "lex errors: {errors:?}");
920 assert_eq!(
921 render_lossless(src, &tokens, &trivia).as_deref(),
922 Ok(src),
923 "round trip failed for {src:?}"
924 );
925 }
926
927 #[test]
928 fn line_comment_becomes_trivia_with_exact_text_and_span() {
929 let src = "x = 1 -- electing to exercise\ny = 2\n";
930 let trivia = trivia_of(src);
931 assert_eq!(trivia.len(), 1);
932 assert_eq!(trivia[0].kind, TriviaKind::LineComment);
933 assert_eq!(trivia[0].text, "-- electing to exercise");
934 assert_eq!(&src[trivia[0].start..trivia[0].end], trivia[0].text);
935 assert_eq!(trivia[0].pos, Pos { line: 1, column: 7 });
936 }
937
938 #[test]
939 fn nested_block_comment_becomes_one_trivia() {
940 let src = "{- outer {- inner -} still -} x";
941 let trivia = trivia_of(src);
942 assert_eq!(trivia.len(), 1);
943 assert_eq!(trivia[0].kind, TriviaKind::BlockComment);
944 assert_eq!(trivia[0].text, "{- outer {- inner -} still -}");
945 }
946
947 #[test]
948 fn unterminated_block_comment_still_yields_trivia_to_eof() {
949 let (_, trivia, errors) = lex_with_trivia("x {- never closed");
950 assert_eq!(errors.len(), 1);
951 assert_eq!(trivia.len(), 1);
952 assert_eq!(trivia[0].text, "{- never closed");
953 }
954
955 #[test]
956 fn blank_lines_between_items_counted() {
957 let src = "x = 1\n\n\ny = 2\n";
958 let trivia = trivia_of(src);
959 assert_eq!(trivia.len(), 1);
960 assert_eq!(trivia[0].kind, TriviaKind::BlankLines(2));
961 assert_eq!(trivia[0].pos, Pos { line: 2, column: 1 });
962 }
963
964 #[test]
965 fn blank_line_at_file_start_counted() {
966 let trivia = trivia_of("\nx = 1\n");
967 assert_eq!(trivia.len(), 1);
968 assert_eq!(trivia[0].kind, TriviaKind::BlankLines(1));
969 assert_eq!(trivia[0].pos, Pos { line: 1, column: 1 });
970 }
971
972 #[test]
973 fn blank_looking_lines_inside_block_comment_are_not_blank_trivia() {
974 let src = "x = 1 {- a\n\nb -}\ny = 2\n";
975 let trivia = trivia_of(src);
976 assert_eq!(trivia.len(), 1, "{trivia:?}");
977 assert_eq!(trivia[0].kind, TriviaKind::BlockComment);
978 }
979
980 #[test]
981 fn blank_line_between_comments_counted() {
982 let src = "-- a\n\n-- b\nx = 1\n";
983 let kinds: Vec<_> = trivia_of(src).into_iter().map(|t| t.kind).collect();
984 assert_eq!(
985 kinds,
986 vec![
987 TriviaKind::LineComment,
988 TriviaKind::BlankLines(1),
989 TriviaKind::LineComment,
990 ]
991 );
992 }
993
994 #[test]
995 fn cpp_directive_becomes_trivia() {
996 let src = "#ifdef DAML_BIGNUMERIC\nx = 1\n#endif\n";
997 let trivia = trivia_of(src);
998 assert_eq!(trivia.len(), 2);
999 assert!(trivia.iter().all(|t| t.kind == TriviaKind::CppDirective));
1000 assert_eq!(trivia[0].text, "#ifdef DAML_BIGNUMERIC");
1001 }
1002
1003 #[test]
1004 fn round_trip_is_byte_identical() {
1005 assert_round_trip("module M where\n\n-- doc\nf : Int -> Int\nf x = x + 1\n");
1006 assert_round_trip("x = \"tem\\\"plate \\n\" {- block {- nested -} -}\r\ny = 'a'\r\n");
1007 assert_round_trip("\tärger = [1..5] -- ütf\n");
1008 assert_round_trip("s = \"gap \\ \\ here\"\n");
1009 assert_round_trip("#ifdef X\nf = 0x1F\n#endif\n");
1010 assert_round_trip("\n\n \nf = 1 \n ");
1011 assert_round_trip("");
1012 }
1013
1014 #[test]
1015 fn render_lossless_detects_lost_bytes() {
1016 let src = "x = 1 -- comment\n";
1017 let (tokens, mut trivia, _) = lex_with_trivia(src);
1018 trivia.clear(); assert!(render_lossless(src, &tokens, &trivia).is_err());
1020 }
1021
1022 #[test]
1023 fn unicode_identifier() {
1024 assert_eq!(
1025 toks("ärger = 1"),
1026 vec![lower("ärger"), Tok::Op("=".into()), Tok::IntLit("1".into())]
1027 );
1028 let _ = upper("Ülf"); }
1030}