1use codehelion_core::conditional::{ArmPath, ArmTracker, StaticCondition};
19use codehelion_core::frontend::{
20 Diagnostic, DiagnosticKind, LexemeInterner, LiteralKind, SourceSpan, Token, TokenKind,
21};
22
23use crate::dialect::Dialect;
24
25const RAW_STRING_PREFIXES: &[&str] = &["u8R", "LR", "uR", "UR", "R"];
27
28const TEXT_PREFIXES: &[&str] = &["u8", "L", "u", "U"];
30
31const DIGRAPHS: &[(&str, &str)] = &[
33 ("%:%:", "##"),
34 ("<:", "["),
35 (":>", "]"),
36 ("<%", "{"),
37 ("%>", "}"),
38 ("%:", "#"),
39];
40
41const TRIGRAPHS: &[(&str, &str)] = &[
43 ("??=", "#"),
44 ("??(", "["),
45 ("??)", "]"),
46 ("??<", "{"),
47 ("??>", "}"),
48 ("??!", "|"),
49 ("??'", "^"),
50 ("??-", "~"),
51];
52
53fn is_ident_start(c: char) -> bool {
54 c.is_alphabetic() || c == '_'
55}
56
57fn is_ident_continue(c: char) -> bool {
58 c.is_alphanumeric() || c == '_'
59}
60
61struct Lexer<'d, 's> {
62 dialect: &'d Dialect,
63 source: &'s str,
64 chars: Vec<char>,
65 byte_at: Vec<usize>,
66 i: usize,
67 line: u32,
68 column: u32,
69 line_has_token: bool,
73 interner: LexemeInterner,
74 tokens: Vec<Token>,
75 diagnostics: Vec<Diagnostic>,
76 conditional_directives: Vec<(usize, ConditionalDirective)>,
77}
78
79#[derive(Clone, Copy)]
81struct Mark {
82 index: usize,
83 line: u32,
84 column: u32,
85}
86
87impl<'d, 's> Lexer<'d, 's> {
88 fn new(source: &'s str, dialect: &'d Dialect) -> Self {
89 let chars: Vec<char> = source.chars().collect();
90 let mut byte_at = Vec::with_capacity(chars.len() + 1);
91 let mut byte = 0;
92 for c in &chars {
93 byte_at.push(byte);
94 byte += c.len_utf8();
95 }
96 byte_at.push(source.len());
97 Self {
98 dialect,
99 source,
100 chars,
101 byte_at,
102 i: 0,
103 line: 1,
104 column: 1,
105 line_has_token: false,
106 interner: LexemeInterner::new(),
107 tokens: Vec::new(),
108 diagnostics: Vec::new(),
109 conditional_directives: Vec::new(),
110 }
111 }
112
113 fn text_from(&self, start: Mark) -> &'s str {
115 &self.source[self.byte_at[start.index]..self.byte_at[self.i]]
116 }
117
118 fn peek(&self, ahead: usize) -> Option<char> {
119 self.chars.get(self.i + ahead).copied()
120 }
121
122 const fn mark(&self) -> Mark {
123 Mark {
124 index: self.i,
125 line: self.line,
126 column: self.column,
127 }
128 }
129
130 fn bump(&mut self) {
132 if let Some(c) = self.chars.get(self.i) {
133 if *c == '\n' {
134 self.line += 1;
135 self.column = 1;
136 } else {
137 self.column += 1;
138 }
139 self.i += 1;
140 }
141 }
142
143 fn span_from(&self, start: Mark) -> SourceSpan {
144 SourceSpan {
145 start_byte: self.byte_at[start.index],
146 end_byte: self.byte_at[self.i],
147 start_line: start.line,
148 start_column: start.column,
149 }
150 }
151
152 fn push(&mut self, kind: TokenKind, start: Mark) {
153 let text = self.interner.intern(self.text_from(start));
154 self.tokens.push(Token {
155 kind,
156 text,
157 span: self.span_from(start),
158 });
159 }
160
161 fn push_normalized(&mut self, kind: TokenKind, start: Mark, text: &str) {
163 let text = self.interner.intern(text);
164 self.tokens.push(Token {
165 kind,
166 text,
167 span: self.span_from(start),
168 });
169 }
170
171 fn diagnose(&mut self, kind: DiagnosticKind, start: Mark) {
172 let span = self.span_from(start);
173 self.diagnostics.push(Diagnostic { kind, span });
174 }
175
176 fn try_line_splice(&mut self) -> bool {
179 let marker_width = if self.peek(0) == Some('\\') {
180 1
181 } else if self.matches_ahead("??/") {
182 3
183 } else {
184 return false;
185 };
186 match (self.peek(marker_width), self.peek(marker_width + 1)) {
187 (Some('\n'), _) => {
188 for _ in 0..=marker_width {
189 self.bump();
190 }
191 true
192 }
193 (Some('\r'), Some('\n')) => {
194 for _ in 0..(marker_width + 2) {
195 self.bump();
196 }
197 true
198 }
199 _ => false,
200 }
201 }
202
203 fn run(self) -> (Vec<Token>, Vec<Diagnostic>) {
204 let (tokens, diagnostics, _) = self.run_with_directives();
205 (tokens, diagnostics)
206 }
207
208 fn run_with_directives(
209 mut self,
210 ) -> (
211 Vec<Token>,
212 Vec<Diagnostic>,
213 Vec<(usize, ConditionalDirective)>,
214 ) {
215 while let Some(c) = self.peek(0) {
216 if self.i == 0 && c == '\u{feff}' {
220 self.i += 1;
221 continue;
222 }
223 if c.is_whitespace() {
224 if c == '\n' {
225 self.line_has_token = false;
226 }
227 self.bump();
228 continue;
229 }
230 if c == '/' && self.peek(1) == Some('/') {
231 self.consume_line_comment();
232 continue;
233 }
234 if c == '/' && self.peek(1) == Some('*') {
235 self.consume_block_comment();
236 continue;
237 }
238 if self.is_directive_marker() && !self.line_has_token {
239 self.consume_directive();
240 continue;
241 }
242 if self.try_line_splice() {
243 continue;
244 }
245 self.line_has_token = true;
246 if self.try_prefixed_literal() {
247 continue;
248 }
249 if c == '"' {
250 let start = self.mark();
251 self.consume_string_from(start);
252 continue;
253 }
254 if c == '\'' {
255 let start = self.mark();
256 self.consume_char_from(start);
257 continue;
258 }
259 if c.is_ascii_digit() || (c == '.' && self.peek(1).is_some_and(|d| d.is_ascii_digit()))
260 {
261 self.consume_number();
262 continue;
263 }
264 if is_ident_start(c) {
265 self.consume_ident();
266 continue;
267 }
268 self.consume_punct();
269 }
270 self.tokens.shrink_to_fit();
273 self.conditional_directives.shrink_to_fit();
274 (self.tokens, self.diagnostics, self.conditional_directives)
275 }
276
277 fn consume_line_comment(&mut self) {
278 while let Some(c) = self.peek(0) {
279 if self.try_line_splice() {
280 continue;
282 }
283 if c == '\n' {
284 break;
285 }
286 self.bump();
287 }
288 }
289
290 fn consume_block_comment(&mut self) {
292 let start = self.mark();
293 self.bump();
295 self.bump();
296 loop {
297 match (self.peek(0), self.peek(1)) {
298 (Some('*'), Some('/')) => {
299 self.bump();
300 self.bump();
301 break;
302 }
303 (Some(_), _) => self.bump(),
304 (None, _) => {
305 self.diagnose(DiagnosticKind::UnterminatedBlockComment, start);
306 break;
307 }
308 }
309 }
310 if self.line > start.line {
314 self.line_has_token = false;
315 }
316 }
317
318 fn consume_directive(&mut self) {
321 let start = self.mark();
322 while let Some(c) = self.peek(0) {
323 if self.try_line_splice() {
324 continue;
325 }
326 if c == '\\' {
327 self.bump();
328 continue;
329 }
330 if c == '\n' {
331 break;
334 }
335 if c == '/' && self.peek(1) == Some('*') {
336 self.consume_block_comment();
337 continue;
338 }
339 if c == '/' && self.peek(1) == Some('/') {
340 self.consume_line_comment();
341 break;
342 }
343 self.bump();
344 }
345 if let Some(directive) = directive(self.text_from(start)) {
346 self.conditional_directives
347 .push((self.byte_at[start.index], directive));
348 }
349 }
350
351 fn matches_ahead(&self, text: &str) -> bool {
352 text.chars()
353 .enumerate()
354 .all(|(k, ch)| self.peek(k) == Some(ch))
355 }
356
357 fn try_prefixed_literal(&mut self) -> bool {
360 if self.dialect.raw_strings {
361 for prefix in RAW_STRING_PREFIXES {
362 if self.matches_ahead(prefix) && self.peek(prefix.len()) == Some('"') {
363 let start = self.mark();
364 for _ in 0..prefix.len() {
365 self.bump();
366 }
367 self.consume_raw_string_body(start);
368 return true;
369 }
370 }
371 }
372 for prefix in TEXT_PREFIXES {
373 if !self.matches_ahead(prefix) {
374 continue;
375 }
376 match self.peek(prefix.len()) {
377 Some('"') => {
378 let start = self.mark();
379 for _ in 0..prefix.len() {
380 self.bump();
381 }
382 self.consume_string_from(start);
383 return true;
384 }
385 Some('\'') => {
386 let start = self.mark();
387 for _ in 0..prefix.len() {
388 self.bump();
389 }
390 self.consume_char_from(start);
391 return true;
392 }
393 _ => {}
394 }
395 }
396 false
397 }
398
399 fn consume_string_from(&mut self, start: Mark) {
404 self.bump();
406 loop {
407 match self.peek(0) {
408 None | Some('\n') => {
409 self.push(TokenKind::Literal(LiteralKind::String), start);
410 self.diagnose(DiagnosticKind::UnterminatedString, start);
411 return;
412 }
413 Some('\\') => {
414 self.bump();
415 self.bump();
416 }
417 Some('"') => {
418 self.bump();
419 self.push(TokenKind::Literal(LiteralKind::String), start);
420 return;
421 }
422 Some(_) => self.bump(),
423 }
424 }
425 }
426
427 fn consume_char_from(&mut self, start: Mark) {
430 self.bump();
432 loop {
433 match self.peek(0) {
434 None | Some('\n') => {
435 self.push(TokenKind::Literal(LiteralKind::Char), start);
436 self.diagnose(DiagnosticKind::UnterminatedChar, start);
437 return;
438 }
439 Some('\\') => {
440 self.bump();
441 self.bump();
442 }
443 Some('\'') => {
444 self.bump();
445 self.push(TokenKind::Literal(LiteralKind::Char), start);
446 return;
447 }
448 Some(_) => self.bump(),
449 }
450 }
451 }
452
453 fn consume_raw_string_body(&mut self, start: Mark) {
456 self.bump();
458 let mut delim: Vec<char> = Vec::new();
459 loop {
460 match self.peek(0) {
461 Some('(') => {
462 self.bump();
463 break;
464 }
465 Some(c)
466 if c != '"'
467 && c != ')'
468 && c != '\\'
469 && !c.is_whitespace()
470 && delim.len() < 16 =>
471 {
472 delim.push(c);
473 self.bump();
474 }
475 _ => {
476 self.push(TokenKind::Literal(LiteralKind::String), start);
478 self.diagnose(DiagnosticKind::UnterminatedString, start);
479 return;
480 }
481 }
482 }
483 loop {
484 match self.peek(0) {
485 None => {
486 self.push(TokenKind::Literal(LiteralKind::String), start);
487 self.diagnose(DiagnosticKind::UnterminatedString, start);
488 return;
489 }
490 Some(')') => {
491 let closes = delim
492 .iter()
493 .enumerate()
494 .all(|(k, &dc)| self.peek(1 + k) == Some(dc))
495 && self.peek(1 + delim.len()) == Some('"');
496 if closes {
497 for _ in 0..(delim.len() + 2) {
498 self.bump();
499 }
500 self.push(TokenKind::Literal(LiteralKind::String), start);
501 return;
502 }
503 self.bump();
504 }
505 Some(_) => self.bump(),
506 }
507 }
508 }
509
510 fn consume_number(&mut self) {
511 let start = self.mark();
512 let hex = self.peek(0) == Some('0') && matches!(self.peek(1), Some('x' | 'X'));
513 let mut is_float = false;
514 while let Some(ch) = self.peek(0) {
515 if ch == '.' {
516 if self.peek(1) == Some('.') {
518 break;
519 }
520 is_float = true;
521 self.bump();
522 } else if !hex && matches!(ch, 'e' | 'E') {
523 is_float = true;
524 self.bump();
525 if matches!(self.peek(0), Some('+' | '-')) {
526 self.bump();
527 }
528 } else if hex && matches!(ch, 'p' | 'P') {
529 is_float = true;
531 self.bump();
532 if matches!(self.peek(0), Some('+' | '-')) {
533 self.bump();
534 }
535 } else if ch == '\''
536 && self.dialect.digit_separators
537 && self.peek(1).is_some_and(|c| c.is_ascii_alphanumeric())
538 {
539 self.bump();
541 } else if is_ident_continue(ch) {
542 self.bump();
543 } else {
544 break;
545 }
546 }
547 let kind = if is_float {
548 LiteralKind::Float
549 } else {
550 LiteralKind::Integer
551 };
552 self.push(TokenKind::Literal(kind), start);
553 }
554
555 fn consume_ident(&mut self) {
556 let start = self.mark();
557 let mut normalized = String::new();
558 loop {
559 if self.try_line_splice() {
560 continue;
561 }
562 let Some(ch) = self.peek(0) else {
563 break;
564 };
565 if !is_ident_continue(ch) {
566 break;
567 }
568 normalized.push(ch);
569 self.bump();
570 }
571 let kind = if self.dialect.keywords.contains(&normalized.as_str()) {
572 TokenKind::Keyword
573 } else {
574 TokenKind::Identifier
575 };
576 self.push_normalized(kind, start, &normalized);
577 }
578
579 fn consume_punct(&mut self) {
580 let start = self.mark();
581 for &(spelling, normalized) in DIGRAPHS.iter().chain(TRIGRAPHS) {
582 if spelling == "<:"
588 && self.dialect.multi_punct.contains(&"::")
589 && self.matches_ahead("<::")
590 && !matches!(self.peek(3), Some(':' | '>'))
591 {
592 continue;
593 }
594 if self.matches_ahead(spelling) {
595 for _ in spelling.chars() {
596 self.bump();
597 }
598 self.push_normalized(TokenKind::Punctuation, start, normalized);
599 return;
600 }
601 }
602 for op in self.dialect.multi_punct {
603 if self.matches_ahead(op) {
604 for _ in 0..op.chars().count() {
605 self.bump();
606 }
607 self.push(TokenKind::Punctuation, start);
608 return;
609 }
610 }
611 let c = self.peek(0).unwrap_or('\0');
614 self.bump();
615 if c.is_ascii() && !c.is_alphanumeric() {
616 self.push(TokenKind::Punctuation, start);
617 } else {
618 self.push(TokenKind::Unknown, start);
619 self.diagnose(DiagnosticKind::UnexpectedCharacter, start);
620 }
621 }
622
623 fn is_directive_marker(&self) -> bool {
624 self.peek(0) == Some('#') || self.matches_ahead("%:") || self.matches_ahead("??=")
625 }
626}
627
628#[must_use]
630pub fn lex(source: &str, dialect: &Dialect) -> (Vec<Token>, Vec<Diagnostic>) {
631 Lexer::new(source, dialect).run()
632}
633
634#[must_use]
641pub fn conditional_paths(source: &str, tokens: &[Token], dialect: &Dialect) -> Vec<ArmPath> {
642 let (_, _, directives) = Lexer::new(source, dialect).run_with_directives();
643 if !directives_are_balanced(&directives) {
644 return vec![ArmPath::default(); tokens.len()];
645 }
646 let mut next_directive = 0usize;
647 let mut tracker = ArmTracker::default();
648 let mut paths = Vec::with_capacity(tokens.len());
649 for token in tokens {
650 while directives
651 .get(next_directive)
652 .is_some_and(|(offset, _)| *offset < token.span.start_byte)
653 {
654 apply_directive(&mut tracker, directives[next_directive].1);
655 next_directive += 1;
656 }
657 paths.push(tracker.current());
658 }
659 paths
660}
661
662fn directives_are_balanced(directives: &[(usize, ConditionalDirective)]) -> bool {
665 let mut depth = 0usize;
666 for (_, directive) in directives {
667 match directive {
668 ConditionalDirective::Begin(_) => depth = depth.saturating_add(1),
669 ConditionalDirective::Next(_) | ConditionalDirective::End if depth == 0 => {
670 return false;
671 }
672 ConditionalDirective::Next(_) => {}
673 ConditionalDirective::End => depth -= 1,
674 }
675 }
676 depth == 0
677}
678
679#[derive(Clone, Copy)]
681enum ConditionalDirective {
682 Begin(StaticCondition),
684 Next(StaticCondition),
686 End,
688}
689
690fn directive(line: &str) -> Option<ConditionalDirective> {
692 let line = line.trim_start_matches([' ', '\t', '\r']);
693 let line = line
694 .strip_prefix('#')
695 .or_else(|| line.strip_prefix("%:"))
696 .or_else(|| line.strip_prefix("??="))?
697 .trim_start();
698 let word_end = line
699 .find(|ch: char| !ch.is_ascii_alphabetic())
700 .unwrap_or(line.len());
701 let (word, tail) = line.split_at(word_end);
702 let condition = static_condition(tail);
703 match word {
704 "if" => Some(ConditionalDirective::Begin(condition)),
705 "ifdef" | "ifndef" => Some(ConditionalDirective::Begin(StaticCondition::Unknown)),
706 "elif" => Some(ConditionalDirective::Next(condition)),
707 "elifdef" | "elifndef" | "else" => {
708 Some(ConditionalDirective::Next(StaticCondition::Unknown))
709 }
710 "endif" => Some(ConditionalDirective::End),
711 _ => None,
712 }
713}
714
715fn static_condition(tail: &str) -> StaticCondition {
717 let tail = tail
718 .split_once("//")
719 .map_or(tail, |(before, _)| before)
720 .trim();
721 match tail {
722 "0" => StaticCondition::False,
723 "1" => StaticCondition::True,
724 _ => StaticCondition::Unknown,
725 }
726}
727
728fn apply_directive(tracker: &mut ArmTracker, directive: ConditionalDirective) {
730 match directive {
731 ConditionalDirective::Begin(condition) => tracker.begin(condition),
732 ConditionalDirective::Next(condition) => tracker.next_arm(condition),
733 ConditionalDirective::End => tracker.end(),
734 }
735}
736
737#[cfg(test)]
738#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
739mod tests {
740 use super::*;
741 use crate::dialect;
742
743 fn lex_c(source: &str) -> (Vec<Token>, Vec<Diagnostic>) {
744 lex(source, &dialect::C)
745 }
746
747 fn texts(source: &str) -> Vec<String> {
748 lex_c(source).0.iter().map(|t| t.text.to_string()).collect()
749 }
750
751 #[test]
752 fn splits_keywords_identifiers_and_operators() {
753 let (tokens, diags) = lex_c("static int add(int a, struct pair *p) { return a + p->x; }");
754 assert!(diags.is_empty());
755 let pairs: Vec<_> = tokens.iter().map(|t| (t.kind, t.text.as_str())).collect();
756 assert_eq!(pairs[0], (TokenKind::Keyword, "static"));
757 assert_eq!(pairs[1], (TokenKind::Keyword, "int"));
758 assert_eq!(pairs[2], (TokenKind::Identifier, "add"));
759 assert!(pairs.contains(&(TokenKind::Punctuation, "->")));
760 assert!(pairs.contains(&(TokenKind::Keyword, "struct")));
761 }
762
763 #[test]
764 fn drops_comments_and_whitespace() {
765 let src = "int x; // trailing\n/* block\nspanning lines */ int y;";
766 let texts = texts(src);
767 assert!(!texts.iter().any(|t| t.contains("trailing")));
768 assert!(!texts.iter().any(|t| t.contains("spanning")));
769 assert!(texts.contains(&"x".to_string()));
770 assert!(texts.contains(&"y".to_string()));
771 }
772
773 #[test]
774 fn preprocessor_directives_are_dropped_whole() {
775 let src = "#include <stdio.h>\n#define TWICE(x) \\\n ((x) + (x))\nint y;\n";
776 let (tokens, diags) = lex_c(src);
777 assert!(diags.is_empty());
778 let texts: Vec<_> = tokens.iter().map(|t| t.text.as_str()).collect();
779 assert_eq!(texts, vec!["int", "y", ";"]);
780 }
781
782 #[test]
783 fn directive_after_a_multiline_comment_is_still_a_directive() {
784 let src = "int x; /* comment\nspanning */ #define GONE 1\nint y;";
785 let texts = texts(src);
786 assert_eq!(texts, vec!["int", "x", ";", "int", "y", ";"]);
787 }
788
789 #[test]
790 fn a_hash_after_code_on_the_same_line_is_ordinary_punctuation() {
791 let (tokens, _) = lex_c("int a; # 1\nint b;");
793 assert!(
794 tokens
795 .iter()
796 .any(|t| t.kind == TokenKind::Punctuation && t.text == "#")
797 );
798 }
799
800 #[test]
801 fn conditional_compilation_keeps_both_branches() {
802 let src = "#if FLAG\nint a;\n#else\nint b;\n#endif\n";
803 let texts = texts(src);
804 assert_eq!(texts, vec!["int", "a", ";", "int", "b", ";"]);
805 }
806
807 #[test]
808 fn conditional_paths_separate_alternative_arms_and_literal_dead_code() {
809 let src = "#ifdef _WIN32\nint windows_value;\n#else\nint unix_value;\n#endif\n#if 0\nint dead_value;\n#else\nint live_value;\n#endif\n";
810 let (tokens, diagnostics) = lex_c(src);
811 assert!(diagnostics.is_empty());
812 let paths = conditional_paths(src, &tokens, &dialect::C);
813 let path_for = |name: &str| {
814 let index = tokens
815 .iter()
816 .position(|token| token.text == name)
817 .unwrap_or_else(|| panic!("missing {name}"));
818 &paths[index]
819 };
820
821 assert!(path_for("windows_value").excludes(path_for("unix_value")));
822 assert!(path_for("dead_value").is_unreachable());
823 assert!(!path_for("live_value").is_unreachable());
824 }
825
826 #[test]
827 fn unclosed_conditionals_do_not_invent_an_exclusion() {
828 let src = "#ifdef MAYBE\nint first_value;\n#else\nint second_value;\n";
829 let (tokens, diagnostics) = lex_c(src);
830 assert!(diagnostics.is_empty());
831 let paths = conditional_paths(src, &tokens, &dialect::C);
832 let path_for = |name: &str| {
833 let index = tokens
834 .iter()
835 .position(|token| token.text == name)
836 .unwrap_or_else(|| panic!("missing {name}"));
837 &paths[index]
838 };
839
840 assert!(
841 !path_for("first_value").excludes(path_for("second_value")),
842 "malformed directives must not hide a Fast finding"
843 );
844 }
845
846 #[test]
847 fn comment_pseudo_directives_do_not_make_code_unreachable() {
848 let src = "// #if 0\nint still_live;\n// #endif\n";
849 let (tokens, diagnostics) = lex_c(src);
850 assert!(diagnostics.is_empty());
851 let paths = conditional_paths(src, &tokens, &dialect::C);
852 let index = tokens
853 .iter()
854 .position(|token| token.text == "still_live")
855 .unwrap_or_else(|| panic!("missing still_live"));
856 assert!(!paths[index].is_unreachable());
857 }
858
859 #[test]
860 fn strings_and_chars_lex_with_escapes_and_prefixes() {
861 let (tokens, diags) = lex_c("char *s = \"a \\\"q\\\" b\"; char c = 'x'; int m = 'ab';");
862 assert!(diags.is_empty());
863 let strings: Vec<_> = tokens
864 .iter()
865 .filter(|t| t.kind == TokenKind::Literal(LiteralKind::String))
866 .map(|t| t.text.as_str())
867 .collect();
868 assert_eq!(strings, vec!["\"a \\\"q\\\" b\""]);
869 let chars: Vec<_> = tokens
870 .iter()
871 .filter(|t| t.kind == TokenKind::Literal(LiteralKind::Char))
872 .map(|t| t.text.as_str())
873 .collect();
874 assert_eq!(chars, vec!["'x'", "'ab'"]);
875
876 let (tokens, diags) = lex_c("const wchar_t *w = L\"wide\"; int u = u8\"n\"[0];");
877 assert!(diags.is_empty());
878 let strings: Vec<_> = tokens
879 .iter()
880 .filter(|t| t.kind == TokenKind::Literal(LiteralKind::String))
881 .map(|t| t.text.as_str())
882 .collect();
883 assert_eq!(strings, vec!["L\"wide\"", "u8\"n\""]);
884 }
885
886 #[test]
887 fn unterminated_string_recovers_at_the_line_break() {
888 let (tokens, diags) = lex_c("char *s = \"open;\nint next;");
889 assert_eq!(diags.len(), 1);
890 assert_eq!(diags[0].kind, DiagnosticKind::UnterminatedString);
891 assert!(
892 tokens
893 .iter()
894 .any(|t| t.kind == TokenKind::Keyword && t.text == "int"),
895 "lexing must continue on the next line"
896 );
897 }
898
899 #[test]
900 fn unterminated_char_and_block_comment_are_diagnosed() {
901 let (_, diags) = lex_c("char c = 'x\nint y;");
902 assert!(
903 diags
904 .iter()
905 .any(|d| d.kind == DiagnosticKind::UnterminatedChar)
906 );
907 let (_, diags) = lex_c("int x; /* open");
908 assert!(
909 diags
910 .iter()
911 .any(|d| d.kind == DiagnosticKind::UnterminatedBlockComment)
912 );
913 }
914
915 #[test]
916 fn numbers_cover_hex_float_and_suffix_forms() {
917 let (tokens, diags) = lex_c(
918 "int a = 0xFF; double b = 1.5e3; double c = 0x1.8p3; long d = 100UL; float e = .5f; float f = 1.f;",
919 );
920 assert!(diags.is_empty());
921 let by_text = |needle: &str| {
922 tokens
923 .iter()
924 .find(|t| t.text == needle)
925 .unwrap_or_else(|| panic!("token {needle} missing"))
926 .kind
927 };
928 assert_eq!(by_text("0xFF"), TokenKind::Literal(LiteralKind::Integer));
929 assert_eq!(by_text("1.5e3"), TokenKind::Literal(LiteralKind::Float));
930 assert_eq!(by_text("0x1.8p3"), TokenKind::Literal(LiteralKind::Float));
931 assert_eq!(by_text("100UL"), TokenKind::Literal(LiteralKind::Integer));
932 assert_eq!(by_text(".5f"), TokenKind::Literal(LiteralKind::Float));
933 assert_eq!(by_text("1.f"), TokenKind::Literal(LiteralKind::Float));
934 }
935
936 #[test]
937 fn recovers_after_an_unexpected_character() {
938 let (tokens, diags) = lex_c("int x = \u{20ac}; int next;");
939 assert_eq!(diags.len(), 1);
940 assert_eq!(diags[0].kind, DiagnosticKind::UnexpectedCharacter);
941 assert!(
942 tokens
943 .iter()
944 .any(|t| t.kind == TokenKind::Identifier && t.text == "next")
945 );
946 }
947
948 #[test]
949 fn line_splices_join_code_lines() {
950 let texts = texts("int a \\\n= 1;");
952 assert_eq!(texts, vec!["int", "a", "=", "1", ";"]);
953 }
954
955 #[test]
956 fn line_splices_inside_identifiers_preserve_one_normalized_token() {
957 let texts = texts("int spl\\\nit = 1; int mo??/\nre = split;");
958 assert_eq!(
959 texts,
960 vec![
961 "int", "split", "=", "1", ";", "int", "more", "=", "split", ";"
962 ]
963 );
964 }
965
966 #[test]
967 fn digraphs_and_trigraphs_use_their_canonical_punctuation() {
968 let texts = texts("%:define COUNT 2\nint values<:COUNT:> = <% 1, 2 %>; int flag ??= 1;");
969 assert_eq!(
970 texts,
971 vec![
972 "int", "values", "[", "COUNT", "]", "=", "{", "1", ",", "2", "}", ";", "int",
973 "flag", "#", "1", ";"
974 ]
975 );
976 }
977
978 #[test]
979 fn raw_strings_do_not_exist_in_c() {
980 let (tokens, diags) = lex_c("R\"(x)\"");
982 assert!(diags.is_empty());
983 assert_eq!(tokens[0].kind, TokenKind::Identifier);
984 assert_eq!(tokens[0].text, "R");
985 assert_eq!(tokens[1].kind, TokenKind::Literal(LiteralKind::String));
986 }
987
988 #[test]
989 fn spans_are_byte_accurate() {
990 let (tokens, _) = lex_c("int x;");
991 let x = tokens.iter().find(|t| t.text == "x").expect("x token");
992 assert_eq!(x.span.start_byte, 4);
993 assert_eq!(x.span.end_byte, 5);
994 assert_eq!(x.span.start_line, 1);
995 }
996
997 #[test]
998 fn skips_a_leading_utf8_bom_without_shifting_source_columns() {
999 let (tokens, diagnostics) = lex_c("\u{feff}int value;");
1000 assert!(diagnostics.is_empty());
1001
1002 let keyword = &tokens[0];
1003 assert_eq!(keyword.kind, TokenKind::Keyword);
1004 assert_eq!(keyword.text, "int");
1005 assert_eq!(keyword.span.start_byte, 3);
1006 assert_eq!(keyword.span.start_line, 1);
1007 assert_eq!(keyword.span.start_column, 1);
1008 }
1009}