1use logos::Logos;
28
29use crate::keywords::{self, Profile};
30use crate::syntax_kind::SyntaxKind;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct Token<'a> {
35 pub kind: SyntaxKind,
36 pub text: &'a str,
37}
38
39#[derive(Logos, Debug, Clone, Copy, PartialEq, Eq)]
43enum RawToken {
44 #[regex("[A-Za-z][A-Za-z0-9_]*")]
49 Ident,
50
51 #[regex(r"[0-9]+\.[0-9]+")]
56 FloatNumber,
57
58 #[regex("[0-9]+")]
62 IntNumber,
63
64 #[token("\"", lex_string)]
68 Str,
69
70 #[token("///", lex_line_comment)]
74 DocCommentLine,
75
76 #[token("//", lex_line_comment)]
78 LineComment,
79
80 #[token("/*", lex_block_comment)]
83 BlockComment(bool),
84
85 #[token(":")]
86 Colon,
87 #[token("=")]
88 Eq,
89 #[token("[")]
90 LBracket,
91 #[token("]")]
92 RBracket,
93 #[token("{")]
94 LBrace,
95 #[token("}")]
96 RBrace,
97 #[token("(")]
98 LParen,
99 #[token(")")]
100 RParen,
101 #[token("..")]
102 DotDot,
103 #[token(".")]
104 Dot,
105 #[token("/")]
106 Slash,
107 #[token(",")]
108 Comma,
109 #[token("?")]
110 Question,
111 #[token(";")]
112 Semicolon,
113 #[token("@")]
114 At,
115 #[token("|")]
116 Pipe,
117 #[token("%")]
118 Percent,
119 #[token("-")]
120 Minus,
121
122 #[token("<")]
127 Lt,
128 #[token("<=")]
129 Le,
130 #[token(">")]
131 Gt,
132 #[token(">=")]
133 Ge,
134 #[token("==")]
135 EqEq,
136 #[token("!=")]
137 Neq,
138 #[token("&&")]
139 AmpAmp,
140 #[token("||")]
141 PipePipe,
142 #[token("!")]
143 Bang,
144 #[token("+")]
145 Plus,
146 #[token("*")]
147 Star,
148
149 #[regex(r"[ \t\r\n]+")]
150 Whitespace,
151}
152
153impl From<RawToken> for SyntaxKind {
154 fn from(token: RawToken) -> Self {
155 match token {
156 RawToken::Ident => SyntaxKind::Ident,
157 RawToken::FloatNumber => SyntaxKind::FloatNumber,
158 RawToken::IntNumber => SyntaxKind::IntNumber,
159 RawToken::Str => SyntaxKind::String,
160 RawToken::DocCommentLine => SyntaxKind::DocComment,
161 RawToken::LineComment => SyntaxKind::LineComment,
162 RawToken::BlockComment(is_doc) => {
163 if is_doc {
164 SyntaxKind::DocComment
165 } else {
166 SyntaxKind::BlockComment
167 }
168 }
169 RawToken::Colon => SyntaxKind::Colon,
170 RawToken::Eq => SyntaxKind::Eq,
171 RawToken::LBracket => SyntaxKind::LBracket,
172 RawToken::RBracket => SyntaxKind::RBracket,
173 RawToken::LBrace => SyntaxKind::LBrace,
174 RawToken::RBrace => SyntaxKind::RBrace,
175 RawToken::LParen => SyntaxKind::LParen,
176 RawToken::RParen => SyntaxKind::RParen,
177 RawToken::DotDot => SyntaxKind::DotDot,
178 RawToken::Dot => SyntaxKind::Dot,
179 RawToken::Slash => SyntaxKind::Slash,
180 RawToken::Comma => SyntaxKind::Comma,
181 RawToken::Question => SyntaxKind::Question,
182 RawToken::Semicolon => SyntaxKind::Semicolon,
183 RawToken::At => SyntaxKind::At,
184 RawToken::Pipe => SyntaxKind::Pipe,
185 RawToken::Percent => SyntaxKind::Percent,
186 RawToken::Minus => SyntaxKind::Minus,
187 RawToken::Lt => SyntaxKind::Lt,
188 RawToken::Le => SyntaxKind::Le,
189 RawToken::Gt => SyntaxKind::Gt,
190 RawToken::Ge => SyntaxKind::Ge,
191 RawToken::EqEq => SyntaxKind::EqEq,
192 RawToken::Neq => SyntaxKind::Neq,
193 RawToken::AmpAmp => SyntaxKind::AmpAmp,
194 RawToken::PipePipe => SyntaxKind::PipePipe,
195 RawToken::Bang => SyntaxKind::Bang,
196 RawToken::Plus => SyntaxKind::Plus,
197 RawToken::Star => SyntaxKind::Star,
198 RawToken::Whitespace => SyntaxKind::Whitespace,
199 }
200 }
201}
202
203struct Raw {
205 kind: SyntaxKind,
206 start: usize,
207 end: usize,
208}
209
210pub fn lex(input: &str, profile: Profile) -> Vec<Token<'_>> {
223 let raws = raw_tokens(input);
224 refine(input, &raws, profile)
225}
226
227fn raw_tokens(input: &str) -> Vec<Raw> {
232 let mut out: Vec<Raw> = Vec::new();
233 let mut prev_kind: Option<SyntaxKind> = None;
237 let mut prev_text: &str = "";
238 let mut base = 0usize;
239 'relex: loop {
240 let mut lexer = RawToken::lexer(&input[base..]);
241 while let Some(result) = lexer.next() {
242 let span = lexer.span();
243 let start = base + span.start;
244 let end = base + span.end;
245 let kind = result.map_or(SyntaxKind::Error, SyntaxKind::from);
246
247 if kind == SyntaxKind::Slash && starts_regex(prev_kind, prev_text) {
248 let (regex_kind, regex_end) = scan_regex(input, start);
249 out.push(Raw {
250 kind: regex_kind,
251 start,
252 end: regex_end,
253 });
254 prev_kind = Some(regex_kind);
255 prev_text = &input[start..regex_end];
256 base = regex_end;
257 continue 'relex;
258 }
259
260 out.push(Raw { kind, start, end });
261 if !kind.is_trivia() {
262 prev_kind = Some(kind);
263 prev_text = &input[start..end];
264 }
265 }
266 break;
267 }
268 out
269}
270
271fn starts_regex(prev_kind: Option<SyntaxKind>, prev_text: &str) -> bool {
278 prev_kind == Some(SyntaxKind::Eq)
279 || (prev_kind == Some(SyntaxKind::Ident) && prev_text == "match")
280}
281
282fn scan_regex(input: &str, start: usize) -> (SyntaxKind, usize) {
288 let bytes = input.as_bytes();
289 let mut i = start + 1; while i < bytes.len() {
291 match bytes[i] {
292 b'\\' => {
293 if i + 1 >= bytes.len() {
294 return (SyntaxKind::Error, bytes.len());
295 }
296 if is_line_break(bytes, i + 1) {
297 return (SyntaxKind::Error, i + 1);
298 }
299 i += 2;
300 }
301 b'/' => return (SyntaxKind::Regex, i + 1),
302 _ if is_line_break(bytes, i) => return (SyntaxKind::Error, i),
303 _ => i += 1,
304 }
305 }
306 (SyntaxKind::Error, bytes.len())
307}
308
309fn refine<'a>(input: &'a str, raws: &[Raw], profile: Profile) -> Vec<Token<'a>> {
313 let mut out = Vec::with_capacity(raws.len());
314 let mut i = 0;
315 while i < raws.len() {
316 let raw = &raws[i];
317
318 if matches!(raw.kind, SyntaxKind::IntNumber | SyntaxKind::FloatNumber)
322 && let Some(next) = raws.get(i + 1)
323 && next.kind == SyntaxKind::Ident
324 && is_time_atom(&input[next.start..next.end])
325 {
326 out.push(Token {
327 kind: SyntaxKind::Duration,
328 text: &input[raw.start..next.end],
329 });
330 i += 2;
331 continue;
332 }
333
334 let text = &input[raw.start..raw.end];
335 let kind = if raw.kind == SyntaxKind::Ident {
336 keywords::keyword_in(profile, text).unwrap_or(if keywords::is_reserved(text) {
337 SyntaxKind::ReservedWord
338 } else {
339 SyntaxKind::Ident
340 })
341 } else {
342 raw.kind
343 };
344 out.push(Token { kind, text });
345 i += 1;
346 }
347 out
348}
349
350fn is_time_atom(text: &str) -> bool {
352 matches!(text, "us" | "ms" | "s" | "min" | "h")
353}
354
355fn lex_string(lex: &mut logos::Lexer<RawToken>) -> Result<(), ()> {
360 let bytes = lex.remainder().as_bytes();
361 let mut i = 0;
362 while i < bytes.len() {
363 match bytes[i] {
364 _ if is_line_break(bytes, i) => {
365 lex.bump(i);
366 return Err(());
367 }
368 b'\\' => {
369 if i + 1 >= bytes.len() || is_line_break(bytes, i + 1) {
370 lex.bump(i + 1);
372 return Err(());
373 }
374 i += 2;
375 }
376 b'"' => {
377 lex.bump(i + 1);
378 return Ok(());
379 }
380 _ => i += 1,
381 }
382 }
383 lex.bump(bytes.len());
384 Err(())
385}
386
387fn is_line_break(bytes: &[u8], i: usize) -> bool {
391 match bytes[i] {
392 b'\n' => true,
393 b'\r' => bytes.get(i + 1) != Some(&b'\n'),
394 _ => false,
395 }
396}
397
398fn lex_line_comment(lex: &mut logos::Lexer<RawToken>) {
402 let bytes = lex.remainder().as_bytes();
403 let end = (0..bytes.len())
404 .find(|&i| is_line_break(bytes, i))
405 .unwrap_or(bytes.len());
406 lex.bump(end);
407}
408
409fn lex_block_comment(lex: &mut logos::Lexer<RawToken>) -> Result<bool, ()> {
414 let rest = lex.remainder();
415 let is_doc = rest.starts_with('*') && !rest.starts_with("*/");
417 match rest.find("*/") {
418 Some(idx) => {
419 lex.bump(idx + 2);
420 Ok(is_doc)
421 }
422 None => {
423 lex.bump(rest.len());
424 Err(())
425 }
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 fn concat(tokens: &[Token<'_>]) -> String {
434 tokens.iter().map(|t| t.text).collect()
435 }
436
437 fn kinds_in(profile: Profile, input: &str) -> Vec<SyntaxKind> {
439 lex(input, profile)
440 .iter()
441 .filter(|t| !t.kind.is_trivia())
442 .map(|t| t.kind)
443 .collect()
444 }
445
446 #[test]
449 fn ridl_words_lex_by_profile() {
450 for (word, kind) in [
451 ("interface", SyntaxKind::InterfaceKw),
452 ("service", SyntaxKind::ServiceKw),
453 ("signal", SyntaxKind::SignalKw),
454 ("event", SyntaxKind::EventKw),
455 ("command", SyntaxKind::CommandKw),
456 ("query", SyntaxKind::QueryKw),
457 ("fixed", SyntaxKind::FixedKw),
458 ("require", SyntaxKind::RequireKw),
459 ("ensure", SyntaxKind::EnsureKw),
460 ] {
461 assert_eq!(
462 kinds_in(Profile::Ridl, word),
463 vec![kind],
464 "`{word}` must lex to its keyword variant under Ridl",
465 );
466 assert_eq!(
467 kinds_in(Profile::Typl, word),
468 vec![SyntaxKind::ReservedWord],
469 "`{word}` must stay ReservedWord under Typl",
470 );
471 }
472 }
473
474 #[test]
475 fn other_profile_words_stay_reserved_in_both_profiles() {
476 for word in ["states", "model", "component", "automaton"] {
477 for profile in [Profile::Typl, Profile::Ridl] {
478 assert_eq!(
479 kinds_in(profile, word),
480 vec![SyntaxKind::ReservedWord],
481 "`{word}` must stay ReservedWord under {profile:?}",
482 );
483 }
484 }
485 }
486
487 const RSDL_WORDS: [(&str, SyntaxKind); 8] = [
490 ("system", SyntaxKind::SystemKw),
491 ("component", SyntaxKind::ComponentKw),
492 ("distribution", SyntaxKind::DistributionKw),
493 ("deployment", SyntaxKind::DeploymentKw),
494 ("machine", SyntaxKind::MachineKw),
495 ("offers", SyntaxKind::OffersKw),
496 ("requires", SyntaxKind::RequiresKw),
497 ("for", SyntaxKind::ForKw),
498 ];
499
500 const RETIRED_RSDL_WORDS: [&str; 13] = [
503 "provides",
504 "instance",
505 "assurance",
506 "target",
507 "place",
508 "on",
509 "transport",
510 "bundle",
511 "time",
512 "base",
513 "redundant",
514 "supervise",
515 "degraded",
516 ];
517
518 #[test]
523 fn rsdl_words_lex_by_profile() {
524 for (word, kind) in RSDL_WORDS {
525 assert_eq!(
526 kinds_in(Profile::Rsdl, word),
527 vec![kind],
528 "`{word}` must lex to its keyword variant under Rsdl",
529 );
530 for profile in [Profile::Typl, Profile::Ridl] {
531 assert_eq!(
532 kinds_in(profile, word),
533 vec![SyntaxKind::ReservedWord],
534 "`{word}` must stay ReservedWord under {profile:?}",
535 );
536 }
537 }
538 }
539
540 #[test]
542 fn retired_rsdl_words_are_identifiers_in_every_profile() {
543 for word in RETIRED_RSDL_WORDS {
544 for profile in [Profile::Typl, Profile::Ridl, Profile::Rsdl] {
545 assert_eq!(
546 kinds_in(profile, word),
547 vec![SyntaxKind::Ident],
548 "`{word}` must lex to Ident under {profile:?}",
549 );
550 }
551 }
552 }
553
554 #[test]
557 fn rsdl_profile_keeps_the_typl_keywords_and_reserves_the_ridl_words() {
558 assert_eq!(
559 kinds_in(Profile::Rsdl, "package"),
560 vec![SyntaxKind::PackageKw]
561 );
562 assert_eq!(
563 kinds_in(Profile::Rsdl, "internal"),
564 vec![SyntaxKind::InternalKw]
565 );
566 assert_eq!(kinds_in(Profile::Rsdl, "true"), vec![SyntaxKind::TrueKw]);
567 assert_eq!(
568 kinds_in(Profile::Rsdl, "service"),
569 vec![SyntaxKind::ReservedWord]
570 );
571 assert_eq!(
572 kinds_in(Profile::Rsdl, "require"),
573 vec![SyntaxKind::ReservedWord]
574 );
575 }
576
577 #[test]
580 fn durations_and_at_lex_clean_under_ridl() {
581 assert_eq!(kinds_in(Profile::Ridl, "10ms"), vec![SyntaxKind::Duration]);
582 assert_eq!(kinds_in(Profile::Ridl, "@"), vec![SyntaxKind::At]);
583 }
584
585 #[test]
587 fn expr_operators_lex_under_both_profiles() {
588 for profile in [Profile::Typl, Profile::Ridl] {
589 assert_eq!(
590 kinds_in(profile, "a >= b && !c"),
591 vec![
592 SyntaxKind::Ident,
593 SyntaxKind::Ge,
594 SyntaxKind::Ident,
595 SyntaxKind::AmpAmp,
596 SyntaxKind::Bang,
597 SyntaxKind::Ident,
598 ],
599 );
600 }
601 }
602
603 #[test]
604 fn every_expr_operator_token_lexes() {
605 assert_eq!(
606 kinds_in(Profile::Ridl, "< <= > >= == != && || ! + *"),
607 vec![
608 SyntaxKind::Lt,
609 SyntaxKind::Le,
610 SyntaxKind::Gt,
611 SyntaxKind::Ge,
612 SyntaxKind::EqEq,
613 SyntaxKind::Neq,
614 SyntaxKind::AmpAmp,
615 SyntaxKind::PipePipe,
616 SyntaxKind::Bang,
617 SyntaxKind::Plus,
618 SyntaxKind::Star,
619 ],
620 );
621 }
622
623 #[test]
625 fn union_pipe_lexes_unchanged() {
626 for profile in [Profile::Typl, Profile::Ridl] {
627 assert_eq!(
628 kinds_in(profile, "T | E"),
629 vec![SyntaxKind::Ident, SyntaxKind::Pipe, SyntaxKind::Ident],
630 );
631 }
632 }
633
634 #[test]
635 fn fixture_round_trips_with_no_errors() {
636 let input = include_str!("../fixtures/walking_skeleton.typl");
637 let tokens = lex(input, Profile::Typl);
638 assert_eq!(concat(&tokens), input, "token texts must reproduce input");
639 assert!(
640 tokens.iter().all(|t| t.kind != SyntaxKind::Error),
641 "fixture must lex with zero Error tokens, got: {tokens:?}"
642 );
643 }
644
645 #[test]
646 fn range_dots_do_not_get_swallowed_by_float_regex() {
647 let tokens = lex("0.0..250.0", Profile::Typl);
648 let kinds: Vec<_> = tokens.iter().map(|t| t.kind).collect();
649 assert_eq!(
650 kinds,
651 vec![
652 SyntaxKind::FloatNumber,
653 SyntaxKind::DotDot,
654 SyntaxKind::FloatNumber,
655 ]
656 );
657 }
658
659 #[test]
660 fn slash_separated_unit_lexes_as_ident_slash_ident() {
661 let tokens = lex("km/h", Profile::Typl);
662 let kinds: Vec<_> = tokens.iter().map(|t| t.kind).collect();
663 assert_eq!(
664 kinds,
665 vec![SyntaxKind::Ident, SyntaxKind::Slash, SyntaxKind::Ident]
666 );
667 }
668
669 #[test]
670 fn line_comment_is_one_token() {
671 let tokens = lex("// x", Profile::Typl);
672 assert_eq!(tokens.len(), 1);
673 assert_eq!(tokens[0].kind, SyntaxKind::LineComment);
674 assert_eq!(tokens[0].text, "// x");
675 }
676
677 #[test]
678 fn unknown_byte_yields_error_and_still_round_trips() {
679 let input = "$";
680 let tokens = lex(input, Profile::Typl);
681 assert_eq!(concat(&tokens), input);
682 assert_eq!(tokens.len(), 1);
683 assert_eq!(tokens[0].kind, SyntaxKind::Error);
684 }
685
686 #[test]
687 fn keywords_lex_distinctly_from_ident() {
688 let tokens = lex("type const step notAKeyword", Profile::Typl);
689 let kinds: Vec<_> = tokens
690 .iter()
691 .filter(|t| t.kind != SyntaxKind::Whitespace)
692 .map(|t| t.kind)
693 .collect();
694 assert_eq!(
695 kinds,
696 vec![
697 SyntaxKind::TypeKw,
698 SyntaxKind::ConstKw,
699 SyntaxKind::StepKw,
700 SyntaxKind::Ident,
701 ]
702 );
703 }
704
705 fn significant_kinds(input: &str) -> Vec<SyntaxKind> {
708 kinds_in(Profile::Typl, input)
709 }
710
711 #[test]
714 fn every_used_keyword_lexes_to_its_variant() {
715 for word in keywords::FAMILY_RESERVED {
716 let tokens = lex(word, Profile::Typl);
717 assert_eq!(tokens.len(), 1, "`{word}` must lex to one token");
718 let expected = keywords::typl_keyword(word).unwrap_or(SyntaxKind::ReservedWord);
719 assert_eq!(tokens[0].kind, expected, "`{word}` lexed to the wrong kind");
720 }
721 }
722
723 #[test]
724 fn reserved_word_is_not_an_identifier() {
725 assert_eq!(significant_kinds("signal"), vec![SyntaxKind::ReservedWord]);
726 assert_eq!(significant_kinds("model"), vec![SyntaxKind::ReservedWord]);
727 assert_eq!(significant_kinds("Speed"), vec![SyntaxKind::Ident]);
728 }
729
730 #[test]
732 fn float_range_lexes_as_float_dotdot_float() {
733 assert_eq!(
734 significant_kinds("0.0..250.0"),
735 vec![
736 SyntaxKind::FloatNumber,
737 SyntaxKind::DotDot,
738 SyntaxKind::FloatNumber,
739 ]
740 );
741 }
742
743 #[test]
746 fn slash_in_unit_is_not_a_regex() {
747 assert_eq!(
748 significant_kinds("km/h"),
749 vec![SyntaxKind::Ident, SyntaxKind::Slash, SyntaxKind::Ident]
750 );
751 }
752
753 #[test]
754 fn regex_literal_after_eq_is_one_token() {
755 let tokens = lex(r"const V = /a\/b/", Profile::Typl);
756 let regex: Vec<_> = tokens
757 .iter()
758 .filter(|t| t.kind == SyntaxKind::Regex)
759 .collect();
760 assert_eq!(regex.len(), 1, "one regex token expected: {tokens:?}");
761 assert_eq!(regex[0].text, r"/a\/b/");
762 }
763
764 #[test]
765 fn unterminated_regex_is_error_to_end_of_line() {
766 let tokens = lex("const V = /abc\n", Profile::Typl);
767 let last_significant = tokens
768 .iter()
769 .rev()
770 .find(|t| !t.kind.is_trivia())
771 .expect("a significant token");
772 assert_eq!(last_significant.kind, SyntaxKind::Error);
773 assert_eq!(last_significant.text, "/abc");
774 }
775
776 #[test]
777 fn inline_match_regex_is_one_token() {
778 let line = r"type Date : string [10 match /^\d{4}-\d{2}-\d{2}$/]";
781 let tokens = lex(line, Profile::Typl);
782 assert!(
783 tokens.iter().all(|t| t.kind != SyntaxKind::Error),
784 "no error tokens expected: {tokens:?}",
785 );
786 assert_eq!(
787 significant_kinds(line),
788 vec![
789 SyntaxKind::TypeKw,
790 SyntaxKind::Ident,
791 SyntaxKind::Colon,
792 SyntaxKind::StringKw,
793 SyntaxKind::LBracket,
794 SyntaxKind::IntNumber,
795 SyntaxKind::MatchKw,
796 SyntaxKind::Regex,
797 SyntaxKind::RBracket,
798 ],
799 );
800 let regex: Vec<_> = tokens
801 .iter()
802 .filter(|t| t.kind == SyntaxKind::Regex)
803 .collect();
804 assert_eq!(regex.len(), 1, "one regex token expected: {tokens:?}");
805 assert_eq!(regex[0].text, r"/^\d{4}-\d{2}-\d{2}$/");
806 }
807
808 #[test]
810 fn durations_lex_as_one_token() {
811 assert_eq!(significant_kinds("10ms"), vec![SyntaxKind::Duration]);
812 assert_eq!(significant_kinds("500us"), vec![SyntaxKind::Duration]);
813 assert_eq!(significant_kinds("1s"), vec![SyntaxKind::Duration]);
814 assert_eq!(significant_kinds("1min"), vec![SyntaxKind::Duration]);
815 assert_eq!(significant_kinds("1.5h"), vec![SyntaxKind::Duration]);
816 }
817
818 #[test]
819 fn bare_time_atom_is_an_identifier() {
820 assert_eq!(significant_kinds("min"), vec![SyntaxKind::Ident]);
821 assert_eq!(
823 significant_kinds("10 ms"),
824 vec![SyntaxKind::IntNumber, SyntaxKind::Ident]
825 );
826 }
827
828 #[test]
830 fn string_with_escape_is_one_token() {
831 let tokens = lex(r#""a\nb""#, Profile::Typl);
832 assert_eq!(tokens.len(), 1);
833 assert_eq!(tokens[0].kind, SyntaxKind::String);
834 assert_eq!(tokens[0].text, r#""a\nb""#);
835 }
836
837 #[test]
838 fn unterminated_string_is_error_to_end_of_line() {
839 let tokens = lex("\"abc\n", Profile::Typl);
840 assert_eq!(tokens[0].kind, SyntaxKind::Error);
841 assert_eq!(tokens[0].text, "\"abc");
842 assert_eq!(tokens[1].kind, SyntaxKind::Whitespace);
844 }
845
846 #[test]
848 fn block_comment_is_one_token() {
849 let tokens = lex("/* block */", Profile::Typl);
850 assert_eq!(tokens.len(), 1);
851 assert_eq!(tokens[0].kind, SyntaxKind::BlockComment);
852 }
853
854 #[test]
855 fn block_comment_ends_at_first_close() {
856 let tokens = lex("/* /* */", Profile::Typl);
857 assert_eq!(tokens.len(), 1, "no nesting: {tokens:?}");
858 assert_eq!(tokens[0].kind, SyntaxKind::BlockComment);
859 assert_eq!(tokens[0].text, "/* /* */");
860 }
861
862 #[test]
863 fn doc_comments_are_distinct_from_plain_comments() {
864 assert_eq!(significant_kinds("/// doc"), Vec::<SyntaxKind>::new());
865 let doc_line = lex("/// doc", Profile::Typl);
866 assert_eq!(doc_line[0].kind, SyntaxKind::DocComment);
867 let doc_block = lex("/** doc */", Profile::Typl);
868 assert_eq!(doc_block[0].kind, SyntaxKind::DocComment);
869 let plain = lex("// doc", Profile::Typl);
870 assert_eq!(plain[0].kind, SyntaxKind::LineComment);
871 }
872
873 #[test]
875 fn leading_zeros_lex_as_one_integer() {
876 let tokens = lex("042", Profile::Typl);
877 assert_eq!(tokens.len(), 1);
878 assert_eq!(tokens[0].kind, SyntaxKind::IntNumber);
879 assert_eq!(tokens[0].text, "042");
880 }
881
882 #[test]
883 fn minus_is_its_own_token() {
884 assert_eq!(
885 significant_kinds("-40"),
886 vec![SyntaxKind::Minus, SyntaxKind::IntNumber]
887 );
888 }
889
890 #[test]
891 fn all_new_punctuation_lexes() {
892 assert_eq!(
893 significant_kinds("{}()?;@|%"),
894 vec![
895 SyntaxKind::LBrace,
896 SyntaxKind::RBrace,
897 SyntaxKind::LParen,
898 SyntaxKind::RParen,
899 SyntaxKind::Question,
900 SyntaxKind::Semicolon,
901 SyntaxKind::At,
902 SyntaxKind::Pipe,
903 SyntaxKind::Percent,
904 ]
905 );
906 }
907
908 fn pairs(input: &str) -> Vec<(SyntaxKind, &str)> {
910 lex(input, Profile::Typl)
911 .iter()
912 .map(|t| (t.kind, t.text))
913 .collect()
914 }
915
916 #[test]
920 fn a_lone_cr_ends_a_line_comment() {
921 assert_eq!(
922 pairs("// a\rx"),
923 vec![
924 (SyntaxKind::LineComment, "// a"),
925 (SyntaxKind::Whitespace, "\r"),
926 (SyntaxKind::Ident, "x"),
927 ],
928 );
929 }
930
931 #[test]
932 fn a_lone_cr_at_the_end_of_input_ends_a_line_comment() {
933 assert_eq!(
934 pairs("// a\r"),
935 vec![
936 (SyntaxKind::LineComment, "// a"),
937 (SyntaxKind::Whitespace, "\r"),
938 ],
939 );
940 }
941
942 #[test]
943 fn a_lone_cr_ends_a_doc_comment_line() {
944 assert_eq!(
945 pairs("/// a\rx"),
946 vec![
947 (SyntaxKind::DocComment, "/// a"),
948 (SyntaxKind::Whitespace, "\r"),
949 (SyntaxKind::Ident, "x"),
950 ],
951 );
952 }
953
954 #[test]
955 fn a_lone_cr_ends_an_unterminated_string() {
956 assert_eq!(
957 pairs("\"abc\rx"),
958 vec![
959 (SyntaxKind::Error, "\"abc"),
960 (SyntaxKind::Whitespace, "\r"),
961 (SyntaxKind::Ident, "x"),
962 ],
963 );
964 assert_eq!(
966 pairs("\"a\\\rx"),
967 vec![
968 (SyntaxKind::Error, "\"a\\"),
969 (SyntaxKind::Whitespace, "\r"),
970 (SyntaxKind::Ident, "x"),
971 ],
972 );
973 }
974
975 #[test]
976 fn a_backslash_and_a_final_lone_cr_end_an_unterminated_string() {
977 assert_eq!(
978 pairs("\"a\\\r"),
979 vec![(SyntaxKind::Error, "\"a\\"), (SyntaxKind::Whitespace, "\r"),],
980 );
981 }
982
983 #[test]
984 fn a_lone_cr_ends_an_unterminated_regex() {
985 let tokens = pairs("const V = /abc\rx");
986 assert_eq!(
987 tokens[tokens.len() - 3..],
988 [
989 (SyntaxKind::Error, "/abc"),
990 (SyntaxKind::Whitespace, "\r"),
991 (SyntaxKind::Ident, "x"),
992 ],
993 );
994 let tokens = pairs("const V = /a\\\rx");
995 assert_eq!(
996 tokens[tokens.len() - 3..],
997 [
998 (SyntaxKind::Error, "/a\\"),
999 (SyntaxKind::Whitespace, "\r"),
1000 (SyntaxKind::Ident, "x"),
1001 ],
1002 );
1003 }
1004
1005 #[test]
1009 fn a_crlf_pair_still_ends_a_line_at_its_lf() {
1010 assert_eq!(
1011 pairs("// a\r\nx"),
1012 vec![
1013 (SyntaxKind::LineComment, "// a\r"),
1014 (SyntaxKind::Whitespace, "\n"),
1015 (SyntaxKind::Ident, "x"),
1016 ],
1017 );
1018 assert_eq!(
1019 pairs("/// a\r\nx"),
1020 vec![
1021 (SyntaxKind::DocComment, "/// a\r"),
1022 (SyntaxKind::Whitespace, "\n"),
1023 (SyntaxKind::Ident, "x"),
1024 ],
1025 );
1026 assert_eq!(
1027 pairs("\"abc\r\nx"),
1028 vec![
1029 (SyntaxKind::Error, "\"abc\r"),
1030 (SyntaxKind::Whitespace, "\n"),
1031 (SyntaxKind::Ident, "x"),
1032 ],
1033 );
1034 let tokens = pairs("const V = /abc\r\nx");
1035 assert_eq!(
1036 tokens[tokens.len() - 3..],
1037 [
1038 (SyntaxKind::Error, "/abc\r"),
1039 (SyntaxKind::Whitespace, "\n"),
1040 (SyntaxKind::Ident, "x"),
1041 ],
1042 );
1043 }
1044}