Skip to main content

codehelion_frontend_c/
lexer.rs

1//! Error-tolerant lexer for the C language family.
2//!
3//! Whitespace and comments are dropped. Preprocessor directives are dropped
4//! whole (through their `\` line continuations): Fast mode does not
5//! preprocess, so both sides of an `#if` stay in the stream as ordinary
6//! tokens while the directive lines themselves never pollute clone content.
7//! Every other lexeme becomes a token carrying its preprocessor-normalized
8//! text and a reporting-only source span. Malformed spans (unterminated strings,
9//! characters and block comments) are recorded as diagnostics and lexing
10//! resumes, so a single broken construct never discards the rest of the file.
11//! Macros are not expanded: an invocation's name and delimiters are ordinary
12//! tokens.
13//!
14//! The lexer is parameterized by a [`Dialect`], which supplies the keyword
15//! set, the operator inventory and the dialect-only literal forms (raw
16//! strings, digit separators), so the same machinery lexes both C and C++.
17
18use 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
25/// Raw-string prefixes, longest first (C++ only; gated by the dialect).
26const RAW_STRING_PREFIXES: &[&str] = &["u8R", "LR", "uR", "UR", "R"];
27
28/// Encoding prefixes of ordinary string and character literals, longest first.
29const TEXT_PREFIXES: &[&str] = &["u8", "L", "u", "U"];
30
31/// C/C++ digraph spellings and their single-token meanings.
32const DIGRAPHS: &[(&str, &str)] = &[
33    ("%:%:", "##"),
34    ("<:", "["),
35    (":>", "]"),
36    ("<%", "{"),
37    ("%>", "}"),
38    ("%:", "#"),
39];
40
41/// C/C++ trigraph spellings other than `??/`, which may form a line splice.
42const 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    /// Whether a token has been emitted on the current line; a `#` may only
70    /// start a preprocessor directive when nothing but whitespace and
71    /// comments precede it on its line.
72    line_has_token: bool,
73    interner: LexemeInterner,
74    tokens: Vec<Token>,
75    diagnostics: Vec<Diagnostic>,
76    conditional_directives: Vec<(usize, ConditionalDirective)>,
77}
78
79/// A position captured at the start of a token.
80#[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    /// The source text between a mark and the current position.
114    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    /// Consume the current character, tracking line and column.
131    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    /// Emit a token with preprocessor-normalized spelling but source span.
162    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    /// Whether a `\` at the current position splices the line; if so consume
177    /// it together with its line break.
178    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            // A UTF-8 BOM is an encoding marker, not source text. Consume it
217            // only at byte zero. Keep its byte width in spans without letting
218            // it shift the source column visible to users.
219            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        // Streams are long-lived (the whole scan holds every file's tokens),
271        // so growth slack is returned to the allocator.
272        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                // A spliced line continues the comment.
281                continue;
282            }
283            if c == '\n' {
284                break;
285            }
286            self.bump();
287        }
288    }
289
290    /// Consume a `/* ... */` comment. C-family block comments do not nest.
291    fn consume_block_comment(&mut self) {
292        let start = self.mark();
293        // Skip the opening `/*`.
294        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        // A comment that crossed onto a new line leaves that line still
311        // "empty": a `#` after it is that line's first token and may start a
312        // directive.
313        if self.line > start.line {
314            self.line_has_token = false;
315        }
316    }
317
318    /// Consume a preprocessor directive from its `#` through the end of the
319    /// logical line, honouring `\` line continuations and embedded comments.
320    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                // Leave the newline for the main loop, which resets the
332                // line state.
333                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    /// Handle encoding-prefixed and raw string/character literals (`L"..."`,
358    /// `u8'...'`, `R"(...)"`, ...). Returns `true` if a token was produced.
359    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    /// Consume a string literal; the current character is the opening `"` and
400    /// `start` marks the beginning of the whole literal (including any
401    /// encoding prefix). An unescaped line break ends the literal with a
402    /// diagnostic, so a missing quote never swallows the rest of the file.
403    fn consume_string_from(&mut self, start: Mark) {
404        // Opening `"`.
405        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    /// Consume a character literal (multi-character constants included); the
428    /// current character is the opening `'`.
429    fn consume_char_from(&mut self, start: Mark) {
430        // Opening `'`.
431        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    /// Consume a raw string body; the current character is the opening `"`
454    /// and `start` marks the whole literal including its `R` prefix.
455    fn consume_raw_string_body(&mut self, start: Mark) {
456        // Opening `"`.
457        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                    // Malformed delimiter: emit what we have as a broken string.
477                    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                // Do not swallow a `...` (GNU case ranges like `1 ... 5`).
517                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                // Hexadecimal floats use a `p` exponent.
530                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                // Digit separator, kept in the raw text.
540                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            // C++ gives `<::` a dedicated maximal-munch exception: unless the
583            // fourth character is `:` or `>`, the `<` is its own token and
584            // the following `::` is scope resolution, not the `<:` digraph
585            // followed by `:`. The C dialect has no `::` operator and keeps
586            // the ordinary digraph rule.
587            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        // Single character. ASCII punctuation is punctuation; anything else
612        // that reached here is not lexable.
613        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/// Lex `source` under `dialect` into tokens and diagnostics.
629#[must_use]
630pub fn lex(source: &str, dialect: &Dialect) -> (Vec<Token>, Vec<Diagnostic>) {
631    Lexer::new(source, dialect).run()
632}
633
634/// Record the preprocessor arm active at each token.
635///
636/// Directives never become clone content, but their nesting determines whether
637/// two otherwise-equal C-family snippets can coexist in one build. This pass
638/// intentionally recognises only literal `0` / `1` conditions; macro and
639/// expression evaluation belongs to a compiler frontend, not Fast mode.
640#[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
662/// Refuse to infer exclusions from an unterminated or otherwise unbalanced
663/// directive sequence. Missing a suppression is safer than hiding a clone.
664fn 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/// One directive that changes the active conditional arm.
680#[derive(Clone, Copy)]
681enum ConditionalDirective {
682    /// Enter an `#if`-style condition.
683    Begin(StaticCondition),
684    /// Enter an `#elif` or `#else` arm.
685    Next(StaticCondition),
686    /// Leave an `#endif`.
687    End,
688}
689
690/// Classify one lexically recognised directive line.
691fn 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
715/// Recognise only a whole literal condition; anything richer stays unknown.
716fn 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
728/// Apply a recognised directive to the current lexical arm.
729fn 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        // Not a directive: `#` is not the first token of its line.
792        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        // A backslash-newline inside ordinary code is consumed as whitespace.
951        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        // `R"(x)"` in C is the identifier `R` followed by an ordinary string.
981        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}