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 #[regex(r"///[^\n]*", priority = 5, allow_greedy = true)]
73 DocCommentLine,
74
75 #[regex(r"//[^\n]*", priority = 4, allow_greedy = true)]
77 LineComment,
78
79 #[token("/*", lex_block_comment)]
82 BlockComment(bool),
83
84 #[token(":")]
85 Colon,
86 #[token("=")]
87 Eq,
88 #[token("[")]
89 LBracket,
90 #[token("]")]
91 RBracket,
92 #[token("{")]
93 LBrace,
94 #[token("}")]
95 RBrace,
96 #[token("(")]
97 LParen,
98 #[token(")")]
99 RParen,
100 #[token("..")]
101 DotDot,
102 #[token(".")]
103 Dot,
104 #[token("/")]
105 Slash,
106 #[token(",")]
107 Comma,
108 #[token("?")]
109 Question,
110 #[token(";")]
111 Semicolon,
112 #[token("@")]
113 At,
114 #[token("|")]
115 Pipe,
116 #[token("%")]
117 Percent,
118 #[token("-")]
119 Minus,
120
121 #[token("<")]
126 Lt,
127 #[token("<=")]
128 Le,
129 #[token(">")]
130 Gt,
131 #[token(">=")]
132 Ge,
133 #[token("==")]
134 EqEq,
135 #[token("!=")]
136 Neq,
137 #[token("&&")]
138 AmpAmp,
139 #[token("||")]
140 PipePipe,
141 #[token("!")]
142 Bang,
143 #[token("+")]
144 Plus,
145 #[token("*")]
146 Star,
147
148 #[regex(r"[ \t\r\n]+")]
149 Whitespace,
150}
151
152impl From<RawToken> for SyntaxKind {
153 fn from(token: RawToken) -> Self {
154 match token {
155 RawToken::Ident => SyntaxKind::Ident,
156 RawToken::FloatNumber => SyntaxKind::FloatNumber,
157 RawToken::IntNumber => SyntaxKind::IntNumber,
158 RawToken::Str => SyntaxKind::String,
159 RawToken::DocCommentLine => SyntaxKind::DocComment,
160 RawToken::LineComment => SyntaxKind::LineComment,
161 RawToken::BlockComment(is_doc) => {
162 if is_doc {
163 SyntaxKind::DocComment
164 } else {
165 SyntaxKind::BlockComment
166 }
167 }
168 RawToken::Colon => SyntaxKind::Colon,
169 RawToken::Eq => SyntaxKind::Eq,
170 RawToken::LBracket => SyntaxKind::LBracket,
171 RawToken::RBracket => SyntaxKind::RBracket,
172 RawToken::LBrace => SyntaxKind::LBrace,
173 RawToken::RBrace => SyntaxKind::RBrace,
174 RawToken::LParen => SyntaxKind::LParen,
175 RawToken::RParen => SyntaxKind::RParen,
176 RawToken::DotDot => SyntaxKind::DotDot,
177 RawToken::Dot => SyntaxKind::Dot,
178 RawToken::Slash => SyntaxKind::Slash,
179 RawToken::Comma => SyntaxKind::Comma,
180 RawToken::Question => SyntaxKind::Question,
181 RawToken::Semicolon => SyntaxKind::Semicolon,
182 RawToken::At => SyntaxKind::At,
183 RawToken::Pipe => SyntaxKind::Pipe,
184 RawToken::Percent => SyntaxKind::Percent,
185 RawToken::Minus => SyntaxKind::Minus,
186 RawToken::Lt => SyntaxKind::Lt,
187 RawToken::Le => SyntaxKind::Le,
188 RawToken::Gt => SyntaxKind::Gt,
189 RawToken::Ge => SyntaxKind::Ge,
190 RawToken::EqEq => SyntaxKind::EqEq,
191 RawToken::Neq => SyntaxKind::Neq,
192 RawToken::AmpAmp => SyntaxKind::AmpAmp,
193 RawToken::PipePipe => SyntaxKind::PipePipe,
194 RawToken::Bang => SyntaxKind::Bang,
195 RawToken::Plus => SyntaxKind::Plus,
196 RawToken::Star => SyntaxKind::Star,
197 RawToken::Whitespace => SyntaxKind::Whitespace,
198 }
199 }
200}
201
202struct Raw {
204 kind: SyntaxKind,
205 start: usize,
206 end: usize,
207}
208
209pub fn lex(input: &str, profile: Profile) -> Vec<Token<'_>> {
222 let raws = raw_tokens(input);
223 refine(input, &raws, profile)
224}
225
226fn raw_tokens(input: &str) -> Vec<Raw> {
231 let mut out: Vec<Raw> = Vec::new();
232 let mut prev_kind: Option<SyntaxKind> = None;
236 let mut prev_text: &str = "";
237 let mut base = 0usize;
238 'relex: loop {
239 let mut lexer = RawToken::lexer(&input[base..]);
240 while let Some(result) = lexer.next() {
241 let span = lexer.span();
242 let start = base + span.start;
243 let end = base + span.end;
244 let kind = result.map_or(SyntaxKind::Error, SyntaxKind::from);
245
246 if kind == SyntaxKind::Slash && starts_regex(prev_kind, prev_text) {
247 let (regex_kind, regex_end) = scan_regex(input, start);
248 out.push(Raw {
249 kind: regex_kind,
250 start,
251 end: regex_end,
252 });
253 prev_kind = Some(regex_kind);
254 prev_text = &input[start..regex_end];
255 base = regex_end;
256 continue 'relex;
257 }
258
259 out.push(Raw { kind, start, end });
260 if !kind.is_trivia() {
261 prev_kind = Some(kind);
262 prev_text = &input[start..end];
263 }
264 }
265 break;
266 }
267 out
268}
269
270fn starts_regex(prev_kind: Option<SyntaxKind>, prev_text: &str) -> bool {
277 prev_kind == Some(SyntaxKind::Eq)
278 || (prev_kind == Some(SyntaxKind::Ident) && prev_text == "match")
279}
280
281fn scan_regex(input: &str, start: usize) -> (SyntaxKind, usize) {
287 let bytes = input.as_bytes();
288 let mut i = start + 1; while i < bytes.len() {
290 match bytes[i] {
291 b'\\' => {
292 if i + 1 >= bytes.len() {
293 return (SyntaxKind::Error, bytes.len());
294 }
295 if bytes[i + 1] == b'\n' {
296 return (SyntaxKind::Error, i + 1);
297 }
298 i += 2;
299 }
300 b'/' => return (SyntaxKind::Regex, i + 1),
301 b'\n' => return (SyntaxKind::Error, i),
302 _ => i += 1,
303 }
304 }
305 (SyntaxKind::Error, bytes.len())
306}
307
308fn refine<'a>(input: &'a str, raws: &[Raw], profile: Profile) -> Vec<Token<'a>> {
312 let mut out = Vec::with_capacity(raws.len());
313 let mut i = 0;
314 while i < raws.len() {
315 let raw = &raws[i];
316
317 if matches!(raw.kind, SyntaxKind::IntNumber | SyntaxKind::FloatNumber)
321 && let Some(next) = raws.get(i + 1)
322 && next.kind == SyntaxKind::Ident
323 && is_time_atom(&input[next.start..next.end])
324 {
325 out.push(Token {
326 kind: SyntaxKind::Duration,
327 text: &input[raw.start..next.end],
328 });
329 i += 2;
330 continue;
331 }
332
333 let text = &input[raw.start..raw.end];
334 let kind = if raw.kind == SyntaxKind::Ident {
335 keywords::keyword_in(profile, text).unwrap_or(if keywords::is_reserved(text) {
336 SyntaxKind::ReservedWord
337 } else {
338 SyntaxKind::Ident
339 })
340 } else {
341 raw.kind
342 };
343 out.push(Token { kind, text });
344 i += 1;
345 }
346 out
347}
348
349fn is_time_atom(text: &str) -> bool {
351 matches!(text, "us" | "ms" | "s" | "min" | "h")
352}
353
354fn lex_string(lex: &mut logos::Lexer<RawToken>) -> Result<(), ()> {
359 let bytes = lex.remainder().as_bytes();
360 let mut i = 0;
361 while i < bytes.len() {
362 match bytes[i] {
363 b'\n' => {
364 lex.bump(i);
365 return Err(());
366 }
367 b'\\' => {
368 if i + 1 >= bytes.len() || bytes[i + 1] == b'\n' {
369 lex.bump(i + 1);
371 return Err(());
372 }
373 i += 2;
374 }
375 b'"' => {
376 lex.bump(i + 1);
377 return Ok(());
378 }
379 _ => i += 1,
380 }
381 }
382 lex.bump(bytes.len());
383 Err(())
384}
385
386fn lex_block_comment(lex: &mut logos::Lexer<RawToken>) -> Result<bool, ()> {
391 let rest = lex.remainder();
392 let is_doc = rest.starts_with('*') && !rest.starts_with("*/");
394 match rest.find("*/") {
395 Some(idx) => {
396 lex.bump(idx + 2);
397 Ok(is_doc)
398 }
399 None => {
400 lex.bump(rest.len());
401 Err(())
402 }
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 fn concat(tokens: &[Token<'_>]) -> String {
411 tokens.iter().map(|t| t.text).collect()
412 }
413
414 fn kinds_in(profile: Profile, input: &str) -> Vec<SyntaxKind> {
416 lex(input, profile)
417 .iter()
418 .filter(|t| !t.kind.is_trivia())
419 .map(|t| t.kind)
420 .collect()
421 }
422
423 #[test]
426 fn ridl_words_lex_by_profile() {
427 for (word, kind) in [
428 ("interface", SyntaxKind::InterfaceKw),
429 ("service", SyntaxKind::ServiceKw),
430 ("signal", SyntaxKind::SignalKw),
431 ("event", SyntaxKind::EventKw),
432 ("command", SyntaxKind::CommandKw),
433 ("query", SyntaxKind::QueryKw),
434 ("fixed", SyntaxKind::FixedKw),
435 ("require", SyntaxKind::RequireKw),
436 ("ensure", SyntaxKind::EnsureKw),
437 ] {
438 assert_eq!(
439 kinds_in(Profile::Ridl, word),
440 vec![kind],
441 "`{word}` must lex to its keyword variant under Ridl",
442 );
443 assert_eq!(
444 kinds_in(Profile::Typl, word),
445 vec![SyntaxKind::ReservedWord],
446 "`{word}` must stay ReservedWord under Typl",
447 );
448 }
449 }
450
451 #[test]
452 fn other_profile_words_stay_reserved_in_both_profiles() {
453 for word in ["view", "model", "component", "automaton"] {
454 for profile in [Profile::Typl, Profile::Ridl] {
455 assert_eq!(
456 kinds_in(profile, word),
457 vec![SyntaxKind::ReservedWord],
458 "`{word}` must stay ReservedWord under {profile:?}",
459 );
460 }
461 }
462 }
463
464 const RSDL_WORDS: [(&str, SyntaxKind); 8] = [
467 ("system", SyntaxKind::SystemKw),
468 ("component", SyntaxKind::ComponentKw),
469 ("distribution", SyntaxKind::DistributionKw),
470 ("deployment", SyntaxKind::DeploymentKw),
471 ("machine", SyntaxKind::MachineKw),
472 ("offers", SyntaxKind::OffersKw),
473 ("requires", SyntaxKind::RequiresKw),
474 ("for", SyntaxKind::ForKw),
475 ];
476
477 const RETIRED_RSDL_WORDS: [&str; 13] = [
480 "provides",
481 "instance",
482 "assurance",
483 "target",
484 "place",
485 "on",
486 "transport",
487 "bundle",
488 "time",
489 "base",
490 "redundant",
491 "supervise",
492 "degraded",
493 ];
494
495 #[test]
500 fn rsdl_words_lex_by_profile() {
501 for (word, kind) in RSDL_WORDS {
502 assert_eq!(
503 kinds_in(Profile::Rsdl, word),
504 vec![kind],
505 "`{word}` must lex to its keyword variant under Rsdl",
506 );
507 for profile in [Profile::Typl, Profile::Ridl] {
508 assert_eq!(
509 kinds_in(profile, word),
510 vec![SyntaxKind::ReservedWord],
511 "`{word}` must stay ReservedWord under {profile:?}",
512 );
513 }
514 }
515 }
516
517 #[test]
519 fn retired_rsdl_words_are_identifiers_in_every_profile() {
520 for word in RETIRED_RSDL_WORDS {
521 for profile in [Profile::Typl, Profile::Ridl, Profile::Rsdl] {
522 assert_eq!(
523 kinds_in(profile, word),
524 vec![SyntaxKind::Ident],
525 "`{word}` must lex to Ident under {profile:?}",
526 );
527 }
528 }
529 }
530
531 #[test]
534 fn rsdl_profile_keeps_the_typl_keywords_and_reserves_the_ridl_words() {
535 assert_eq!(
536 kinds_in(Profile::Rsdl, "package"),
537 vec![SyntaxKind::PackageKw]
538 );
539 assert_eq!(
540 kinds_in(Profile::Rsdl, "internal"),
541 vec![SyntaxKind::InternalKw]
542 );
543 assert_eq!(kinds_in(Profile::Rsdl, "true"), vec![SyntaxKind::TrueKw]);
544 assert_eq!(
545 kinds_in(Profile::Rsdl, "service"),
546 vec![SyntaxKind::ReservedWord]
547 );
548 assert_eq!(
549 kinds_in(Profile::Rsdl, "require"),
550 vec![SyntaxKind::ReservedWord]
551 );
552 }
553
554 #[test]
557 fn durations_and_at_lex_clean_under_ridl() {
558 assert_eq!(kinds_in(Profile::Ridl, "10ms"), vec![SyntaxKind::Duration]);
559 assert_eq!(kinds_in(Profile::Ridl, "@"), vec![SyntaxKind::At]);
560 }
561
562 #[test]
564 fn expr_operators_lex_under_both_profiles() {
565 for profile in [Profile::Typl, Profile::Ridl] {
566 assert_eq!(
567 kinds_in(profile, "a >= b && !c"),
568 vec![
569 SyntaxKind::Ident,
570 SyntaxKind::Ge,
571 SyntaxKind::Ident,
572 SyntaxKind::AmpAmp,
573 SyntaxKind::Bang,
574 SyntaxKind::Ident,
575 ],
576 );
577 }
578 }
579
580 #[test]
581 fn every_expr_operator_token_lexes() {
582 assert_eq!(
583 kinds_in(Profile::Ridl, "< <= > >= == != && || ! + *"),
584 vec![
585 SyntaxKind::Lt,
586 SyntaxKind::Le,
587 SyntaxKind::Gt,
588 SyntaxKind::Ge,
589 SyntaxKind::EqEq,
590 SyntaxKind::Neq,
591 SyntaxKind::AmpAmp,
592 SyntaxKind::PipePipe,
593 SyntaxKind::Bang,
594 SyntaxKind::Plus,
595 SyntaxKind::Star,
596 ],
597 );
598 }
599
600 #[test]
602 fn union_pipe_lexes_unchanged() {
603 for profile in [Profile::Typl, Profile::Ridl] {
604 assert_eq!(
605 kinds_in(profile, "T | E"),
606 vec![SyntaxKind::Ident, SyntaxKind::Pipe, SyntaxKind::Ident],
607 );
608 }
609 }
610
611 #[test]
612 fn fixture_round_trips_with_no_errors() {
613 let input = include_str!("../fixtures/walking_skeleton.typl");
614 let tokens = lex(input, Profile::Typl);
615 assert_eq!(concat(&tokens), input, "token texts must reproduce input");
616 assert!(
617 tokens.iter().all(|t| t.kind != SyntaxKind::Error),
618 "fixture must lex with zero Error tokens, got: {tokens:?}"
619 );
620 }
621
622 #[test]
623 fn range_dots_do_not_get_swallowed_by_float_regex() {
624 let tokens = lex("0.0..250.0", Profile::Typl);
625 let kinds: Vec<_> = tokens.iter().map(|t| t.kind).collect();
626 assert_eq!(
627 kinds,
628 vec![
629 SyntaxKind::FloatNumber,
630 SyntaxKind::DotDot,
631 SyntaxKind::FloatNumber,
632 ]
633 );
634 }
635
636 #[test]
637 fn slash_separated_unit_lexes_as_ident_slash_ident() {
638 let tokens = lex("km/h", Profile::Typl);
639 let kinds: Vec<_> = tokens.iter().map(|t| t.kind).collect();
640 assert_eq!(
641 kinds,
642 vec![SyntaxKind::Ident, SyntaxKind::Slash, SyntaxKind::Ident]
643 );
644 }
645
646 #[test]
647 fn line_comment_is_one_token() {
648 let tokens = lex("// x", Profile::Typl);
649 assert_eq!(tokens.len(), 1);
650 assert_eq!(tokens[0].kind, SyntaxKind::LineComment);
651 assert_eq!(tokens[0].text, "// x");
652 }
653
654 #[test]
655 fn unknown_byte_yields_error_and_still_round_trips() {
656 let input = "$";
657 let tokens = lex(input, Profile::Typl);
658 assert_eq!(concat(&tokens), input);
659 assert_eq!(tokens.len(), 1);
660 assert_eq!(tokens[0].kind, SyntaxKind::Error);
661 }
662
663 #[test]
664 fn keywords_lex_distinctly_from_ident() {
665 let tokens = lex("type const step notAKeyword", Profile::Typl);
666 let kinds: Vec<_> = tokens
667 .iter()
668 .filter(|t| t.kind != SyntaxKind::Whitespace)
669 .map(|t| t.kind)
670 .collect();
671 assert_eq!(
672 kinds,
673 vec![
674 SyntaxKind::TypeKw,
675 SyntaxKind::ConstKw,
676 SyntaxKind::StepKw,
677 SyntaxKind::Ident,
678 ]
679 );
680 }
681
682 fn significant_kinds(input: &str) -> Vec<SyntaxKind> {
685 kinds_in(Profile::Typl, input)
686 }
687
688 #[test]
691 fn every_used_keyword_lexes_to_its_variant() {
692 for word in keywords::FAMILY_RESERVED {
693 let tokens = lex(word, Profile::Typl);
694 assert_eq!(tokens.len(), 1, "`{word}` must lex to one token");
695 let expected = keywords::typl_keyword(word).unwrap_or(SyntaxKind::ReservedWord);
696 assert_eq!(tokens[0].kind, expected, "`{word}` lexed to the wrong kind");
697 }
698 }
699
700 #[test]
701 fn reserved_word_is_not_an_identifier() {
702 assert_eq!(significant_kinds("signal"), vec![SyntaxKind::ReservedWord]);
703 assert_eq!(significant_kinds("model"), vec![SyntaxKind::ReservedWord]);
704 assert_eq!(significant_kinds("Speed"), vec![SyntaxKind::Ident]);
705 }
706
707 #[test]
709 fn float_range_lexes_as_float_dotdot_float() {
710 assert_eq!(
711 significant_kinds("0.0..250.0"),
712 vec![
713 SyntaxKind::FloatNumber,
714 SyntaxKind::DotDot,
715 SyntaxKind::FloatNumber,
716 ]
717 );
718 }
719
720 #[test]
723 fn slash_in_unit_is_not_a_regex() {
724 assert_eq!(
725 significant_kinds("km/h"),
726 vec![SyntaxKind::Ident, SyntaxKind::Slash, SyntaxKind::Ident]
727 );
728 }
729
730 #[test]
731 fn regex_literal_after_eq_is_one_token() {
732 let tokens = lex(r"const V = /a\/b/", Profile::Typl);
733 let regex: Vec<_> = tokens
734 .iter()
735 .filter(|t| t.kind == SyntaxKind::Regex)
736 .collect();
737 assert_eq!(regex.len(), 1, "one regex token expected: {tokens:?}");
738 assert_eq!(regex[0].text, r"/a\/b/");
739 }
740
741 #[test]
742 fn unterminated_regex_is_error_to_end_of_line() {
743 let tokens = lex("const V = /abc\n", Profile::Typl);
744 let last_significant = tokens
745 .iter()
746 .rev()
747 .find(|t| !t.kind.is_trivia())
748 .expect("a significant token");
749 assert_eq!(last_significant.kind, SyntaxKind::Error);
750 assert_eq!(last_significant.text, "/abc");
751 }
752
753 #[test]
754 fn inline_match_regex_is_one_token() {
755 let line = r"type Date : string [10 match /^\d{4}-\d{2}-\d{2}$/]";
758 let tokens = lex(line, Profile::Typl);
759 assert!(
760 tokens.iter().all(|t| t.kind != SyntaxKind::Error),
761 "no error tokens expected: {tokens:?}",
762 );
763 assert_eq!(
764 significant_kinds(line),
765 vec![
766 SyntaxKind::TypeKw,
767 SyntaxKind::Ident,
768 SyntaxKind::Colon,
769 SyntaxKind::StringKw,
770 SyntaxKind::LBracket,
771 SyntaxKind::IntNumber,
772 SyntaxKind::MatchKw,
773 SyntaxKind::Regex,
774 SyntaxKind::RBracket,
775 ],
776 );
777 let regex: Vec<_> = tokens
778 .iter()
779 .filter(|t| t.kind == SyntaxKind::Regex)
780 .collect();
781 assert_eq!(regex.len(), 1, "one regex token expected: {tokens:?}");
782 assert_eq!(regex[0].text, r"/^\d{4}-\d{2}-\d{2}$/");
783 }
784
785 #[test]
787 fn durations_lex_as_one_token() {
788 assert_eq!(significant_kinds("10ms"), vec![SyntaxKind::Duration]);
789 assert_eq!(significant_kinds("500us"), vec![SyntaxKind::Duration]);
790 assert_eq!(significant_kinds("1s"), vec![SyntaxKind::Duration]);
791 assert_eq!(significant_kinds("1min"), vec![SyntaxKind::Duration]);
792 assert_eq!(significant_kinds("1.5h"), vec![SyntaxKind::Duration]);
793 }
794
795 #[test]
796 fn bare_time_atom_is_an_identifier() {
797 assert_eq!(significant_kinds("min"), vec![SyntaxKind::Ident]);
798 assert_eq!(
800 significant_kinds("10 ms"),
801 vec![SyntaxKind::IntNumber, SyntaxKind::Ident]
802 );
803 }
804
805 #[test]
807 fn string_with_escape_is_one_token() {
808 let tokens = lex(r#""a\nb""#, Profile::Typl);
809 assert_eq!(tokens.len(), 1);
810 assert_eq!(tokens[0].kind, SyntaxKind::String);
811 assert_eq!(tokens[0].text, r#""a\nb""#);
812 }
813
814 #[test]
815 fn unterminated_string_is_error_to_end_of_line() {
816 let tokens = lex("\"abc\n", Profile::Typl);
817 assert_eq!(tokens[0].kind, SyntaxKind::Error);
818 assert_eq!(tokens[0].text, "\"abc");
819 assert_eq!(tokens[1].kind, SyntaxKind::Whitespace);
821 }
822
823 #[test]
825 fn block_comment_is_one_token() {
826 let tokens = lex("/* block */", Profile::Typl);
827 assert_eq!(tokens.len(), 1);
828 assert_eq!(tokens[0].kind, SyntaxKind::BlockComment);
829 }
830
831 #[test]
832 fn block_comment_ends_at_first_close() {
833 let tokens = lex("/* /* */", Profile::Typl);
834 assert_eq!(tokens.len(), 1, "no nesting: {tokens:?}");
835 assert_eq!(tokens[0].kind, SyntaxKind::BlockComment);
836 assert_eq!(tokens[0].text, "/* /* */");
837 }
838
839 #[test]
840 fn doc_comments_are_distinct_from_plain_comments() {
841 assert_eq!(significant_kinds("/// doc"), Vec::<SyntaxKind>::new());
842 let doc_line = lex("/// doc", Profile::Typl);
843 assert_eq!(doc_line[0].kind, SyntaxKind::DocComment);
844 let doc_block = lex("/** doc */", Profile::Typl);
845 assert_eq!(doc_block[0].kind, SyntaxKind::DocComment);
846 let plain = lex("// doc", Profile::Typl);
847 assert_eq!(plain[0].kind, SyntaxKind::LineComment);
848 }
849
850 #[test]
852 fn leading_zeros_lex_as_one_integer() {
853 let tokens = lex("042", Profile::Typl);
854 assert_eq!(tokens.len(), 1);
855 assert_eq!(tokens[0].kind, SyntaxKind::IntNumber);
856 assert_eq!(tokens[0].text, "042");
857 }
858
859 #[test]
860 fn minus_is_its_own_token() {
861 assert_eq!(
862 significant_kinds("-40"),
863 vec![SyntaxKind::Minus, SyntaxKind::IntNumber]
864 );
865 }
866
867 #[test]
868 fn all_new_punctuation_lexes() {
869 assert_eq!(
870 significant_kinds("{}()?;@|%"),
871 vec![
872 SyntaxKind::LBrace,
873 SyntaxKind::RBrace,
874 SyntaxKind::LParen,
875 SyntaxKind::RParen,
876 SyntaxKind::Question,
877 SyntaxKind::Semicolon,
878 SyntaxKind::At,
879 SyntaxKind::Pipe,
880 SyntaxKind::Percent,
881 ]
882 );
883 }
884}