Skip to main content

rucc_lex/
lexer.rs

1//! Phase 3: bytes to preprocessing tokens.
2//!
3//! Design: `spec/05-preprocessor.md` sections 5.1 and 5.2.
4//!
5//! The scanner is a loop over the dispatch table in [`crate::class`]. It never copies the
6//! input, never allocates for a token whose spelling is contiguous in the file, and interns
7//! identifiers as it goes rather than in a second pass, which `spec/06-lexer-and-parser.md`
8//! section 6.1 asks for so that no part of the compiler after this one compares identifier
9//! text.
10
11use rucc_base::{Interner, Symbol};
12use rucc_diag::{BytePos, Diagnostic, Span};
13
14use crate::class::{CLASS, Class, is_ident_continue};
15use crate::cursor::Cursor;
16use crate::token::{PpToken, PpTokenKind, Punct, TokenFlags};
17
18/// The dialect knobs phase 1 cares about.
19#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
20#[non_exhaustive]
21pub struct Options {
22    /// Replace trigraphs, which `-trigraphs` turns on.
23    ///
24    /// Off by default because C23 removed them, and because leaving them on means `??!`
25    /// inside a string literal silently becomes `|`, which has caught out every project that
26    /// ever wrote a question mark next to a punctuator.
27    pub trigraphs: bool,
28}
29
30impl Options {
31    /// The defaults, which are what `-std=gnu23` implies.
32    #[must_use]
33    pub fn new() -> Options {
34        Options { trigraphs: false }
35    }
36}
37
38/// A scanner over one file.
39#[derive(Debug)]
40pub struct Lexer<'a> {
41    cursor: Cursor<'a>,
42    /// Where this file begins in the flat coordinate space `rucc-diag` describes, so that a
43    /// span is comparable across files without carrying a file id.
44    file_start: BytePos,
45    at_line_start: bool,
46    leading_space: bool,
47    /// Where the token currently being scanned began, so that the first interruption in its
48    /// spelling can copy everything before itself in one go.
49    token_start: u32,
50    /// Reused between tokens. Only touched for a token whose spelling is interrupted by a
51    /// splice or a trigraph, which is a small fraction of a real file.
52    scratch: Vec<u8>,
53    /// Whether the token being scanned has needed `scratch`.
54    unclean: bool,
55    diagnostics: Vec<Diagnostic>,
56}
57
58impl<'a> Lexer<'a> {
59    /// A scanner over `src`, whose first byte sits at `file_start`.
60    #[must_use]
61    pub fn new(src: &'a [u8], file_start: BytePos, opts: Options) -> Lexer<'a> {
62        Lexer {
63            cursor: Cursor::new(src, opts.trigraphs),
64            file_start,
65            at_line_start: true,
66            leading_space: false,
67            token_start: 0,
68            scratch: Vec::new(),
69            unclean: false,
70            diagnostics: Vec::new(),
71        }
72    }
73
74    /// Everything the scan has complained about so far.
75    #[must_use]
76    pub fn diagnostics(&self) -> &[Diagnostic] {
77        &self.diagnostics
78    }
79
80    /// Takes the diagnostics, leaving the scanner able to carry on.
81    pub fn take_diagnostics(&mut self) -> Vec<Diagnostic> {
82        std::mem::take(&mut self.diagnostics)
83    }
84
85    /// The next preprocessing token, or an end of file token at the end.
86    pub fn next_token(&mut self, interner: &mut Interner) -> PpToken {
87        let token = self.scan(interner);
88        self.report_loose_splices();
89        token
90    }
91
92    /// Turns the splices the cursor flagged into warnings.
93    ///
94    /// GCC warns about a backslash with whitespace before the line ending and splices anyway.
95    /// Both halves matter: a good deal of real code has a trailing space after a backslash in
96    /// a macro definition and expects it to keep working, and the space is invisible in an
97    /// editor, so the one time it does change the meaning nobody can see why.
98    fn report_loose_splices(&mut self) {
99        for at in self.cursor.take_loose_splices() {
100            let span = Span::new(self.file_start + at, self.file_start + at + 1);
101            self.diagnostics.push(Diagnostic::warning(
102                "backslash and line ending separated by whitespace",
103                span,
104            ));
105        }
106    }
107
108    fn scan(&mut self, interner: &mut Interner) -> PpToken {
109        self.skip_trivia();
110
111        let start = self.cursor.pos();
112        let flags = self.take_flags();
113
114        if self.cursor.at_end() {
115            return PpToken {
116                kind: PpTokenKind::Eof,
117                flags,
118                value: None,
119                span: Span::empty_at(self.file_start + start),
120            };
121        }
122
123        self.token_start = start;
124        self.unclean = false;
125
126        let b = self.cursor.first();
127        let kind = match CLASS[b as usize] {
128            Class::IdentStart => self.ident_or_prefixed_literal(b, start),
129            Class::Digit => self.pp_number(),
130            Class::Dot if CLASS[self.cursor.nth(1) as usize] == Class::Digit => self.pp_number(),
131            Class::Quote => self.literal(b'"', start, PpTokenKind::StringLit),
132            Class::Apostrophe => self.literal(b'\'', start, PpTokenKind::CharConst),
133            Class::Backslash => {
134                // A backslash that survived phase 2 either begins a universal character name,
135                // which is a way of spelling an identifier character, or is a stray.
136                if matches!(self.cursor.nth(1), b'u' | b'U') {
137                    self.identifier()
138                } else {
139                    self.eat();
140                    PpTokenKind::Other
141                }
142            }
143            Class::Dot | Class::Slash | Class::Punct => match self.punctuator(start, flags) {
144                Some(token) => return token,
145                None => {
146                    self.eat();
147                    PpTokenKind::Other
148                }
149            },
150            Class::Space | Class::Newline | Class::Other => {
151                self.eat();
152                PpTokenKind::Other
153            }
154        };
155
156        let end = self.cursor.pos();
157        let value = Some(self.intern_spelling(interner, start, end));
158        let mut flags = flags;
159        if self.unclean {
160            flags = flags.with(TokenFlags::SPLICED);
161        }
162        let span = Span::new(self.file_start + start, self.file_start + end);
163        PpToken { kind, flags, value, span }
164    }
165
166    /// Scans a header name, which only a `#include` line can ask for.
167    ///
168    /// Returns `None` when the line does not begin with `<` or `"`, which is the computed
169    /// include case: the directive has to macro expand the line and try again. The scanner
170    /// cannot make this call itself, because `<stdio.h>` and a pair of comparisons are the
171    /// same bytes and only the directive knows which one is possible here.
172    pub fn header_name(&mut self, interner: &mut Interner) -> Option<PpToken> {
173        let token = self.scan_header_name(interner);
174        self.report_loose_splices();
175        token
176    }
177
178    fn scan_header_name(&mut self, interner: &mut Interner) -> Option<PpToken> {
179        self.skip_horizontal();
180        let start = self.cursor.pos();
181        let close = match self.cursor.first() {
182            b'<' => b'>',
183            b'"' => b'"',
184            _ => return None,
185        };
186        let flags = self.take_flags();
187        self.token_start = start;
188        self.unclean = false;
189        self.eat();
190        loop {
191            if self.cursor.at_end() || self.cursor.first() == b'\n' {
192                let span = Span::new(self.file_start + start, self.file_start + self.cursor.pos());
193                self.diagnostics
194                    .push(Diagnostic::error("missing terminating character in header name", span));
195                break;
196            }
197            if self.eat() == close {
198                break;
199            }
200        }
201        let end = self.cursor.pos();
202        let value = Some(self.intern_spelling(interner, start, end));
203        let mut flags = flags;
204        if self.unclean {
205            flags = flags.with(TokenFlags::SPLICED);
206        }
207        let span = Span::new(self.file_start + start, self.file_start + end);
208        Some(PpToken { kind: PpTokenKind::HeaderName, flags, value, span })
209    }
210
211    /// The flags for the token about to be scanned, and resets them for the next one.
212    fn take_flags(&mut self) -> TokenFlags {
213        let mut flags = TokenFlags::EMPTY;
214        if self.at_line_start {
215            flags = flags.with(TokenFlags::START_OF_LINE);
216        }
217        if self.leading_space {
218            flags = flags.with(TokenFlags::LEADING_SPACE);
219        }
220        self.at_line_start = false;
221        self.leading_space = false;
222        flags
223    }
224
225    /// Consumes one logical byte, keeping the spelling buffer correct.
226    fn eat(&mut self) -> u8 {
227        let before = self.cursor.pos();
228        // Every caller has already established there is a byte here, through `at_end` or
229        // through a lookahead that returned a non-zero byte.
230        let (b, clean) = self.cursor.bump().expect("eat called at end of file");
231        if self.unclean {
232            self.scratch.push(b);
233        } else if !clean {
234            // The first interruption in this token. Everything before it was contiguous, so
235            // it copies as one slice, and only from here on does the scan pay per byte.
236            let from = self.token_start as usize;
237            let bytes = self.cursor.bytes();
238            self.scratch.clear();
239            self.scratch.extend_from_slice(&bytes[from..before as usize]);
240            self.scratch.push(b);
241            self.unclean = true;
242        }
243        b
244    }
245
246    /// Interns the spelling of the token that ran from `start` to `end`.
247    fn intern_spelling(&mut self, interner: &mut Interner, start: u32, end: u32) -> Symbol {
248        let lossy = {
249            let bytes: &[u8] = if self.unclean {
250                &self.scratch
251            } else {
252                &self.cursor.bytes()[start as usize..end as usize]
253            };
254            match std::str::from_utf8(bytes) {
255                Ok(text) => return interner.intern(text),
256                // Only reachable inside an identifier or a literal, because everything else
257                // is ASCII by construction. Lossy rather than fatal, so that one bad byte
258                // does not stop the run before the errors the user cares about.
259                Err(_) => String::from_utf8_lossy(bytes).into_owned(),
260            }
261        };
262        let span = Span::new(self.file_start + start, self.file_start + end);
263        self.diagnostics.push(Diagnostic::error("source is not valid UTF-8 here", span));
264        interner.intern(&lossy)
265    }
266
267    /// Whitespace, newlines and comments, all of which become one space.
268    fn skip_trivia(&mut self) {
269        while !self.cursor.at_end() {
270            // Indentation first, in one jump. This is the single hottest thing the lexer does,
271            // because every line of every header starts with some and none of it says anything.
272            if self.cursor.skip_blanks() {
273                self.leading_space = true;
274                continue;
275            }
276            match CLASS[self.cursor.first() as usize] {
277                Class::Space => {
278                    self.cursor.bump();
279                    self.leading_space = true;
280                }
281                Class::Newline => {
282                    self.cursor.bump();
283                    self.at_line_start = true;
284                    self.leading_space = false;
285                }
286                Class::Slash => match self.cursor.nth(1) {
287                    b'/' => self.line_comment(),
288                    b'*' => self.block_comment(),
289                    _ => return,
290                },
291                _ => return,
292            }
293        }
294    }
295
296    /// Spaces and block comments but not newlines, for scanning inside a directive line.
297    fn skip_horizontal(&mut self) {
298        while !self.cursor.at_end() {
299            if self.cursor.skip_blanks() {
300                self.leading_space = true;
301                continue;
302            }
303            let b = self.cursor.first();
304            if CLASS[b as usize] == Class::Space {
305                self.cursor.bump();
306                self.leading_space = true;
307            } else if b == b'/' && self.cursor.nth(1) == b'*' {
308                self.block_comment();
309            } else {
310                return;
311            }
312        }
313    }
314
315    fn line_comment(&mut self) {
316        while !self.cursor.at_end() && self.cursor.first() != b'\n' {
317            // Nothing in the body means anything, so the only bytes worth stopping on are the
318            // ones that could end it: the newline, and a backslash or trigraph that splices the
319            // comment onto the next line instead. Everything between goes past unread.
320            self.cursor.skip_plain(&[]);
321            if self.cursor.at_end() || self.cursor.first() == b'\n' {
322                break;
323            }
324            self.cursor.bump();
325        }
326        // A comment becomes one space. The newline that ends it is left for the trivia loop,
327        // so the next token still knows it starts a line.
328        self.leading_space = true;
329    }
330
331    fn block_comment(&mut self) {
332        let start = self.cursor.pos();
333        self.cursor.bump();
334        self.cursor.bump();
335        let mut spans_lines = false;
336        loop {
337            // Same trade as the line comment, with `*` added because that is what can end this
338            // one. A license block is a couple of thousand bytes of nothing, and this walks it
339            // in a few dozen steps rather than a few thousand.
340            self.cursor.skip_plain(b"*");
341            if self.cursor.at_end() {
342                let span = Span::new(self.file_start + start, self.file_start + self.cursor.pos());
343                self.diagnostics.push(Diagnostic::error("unterminated comment", span));
344                break;
345            }
346            let b = self.cursor.first();
347            if b == b'\n' {
348                spans_lines = true;
349            }
350            if b == b'*' && self.cursor.nth(1) == b'/' {
351                self.cursor.bump();
352                self.cursor.bump();
353                break;
354            }
355            self.cursor.bump();
356        }
357        self.leading_space = true;
358        if spans_lines {
359            // A comment is whitespace, so a `#` after a comment that crossed a newline is
360            // still the first thing on its line and is still a directive. GCC agrees, and
361            // real headers write directives this way.
362            self.at_line_start = true;
363        }
364    }
365
366    fn ident_or_prefixed_literal(&mut self, b: u8, start: u32) -> PpTokenKind {
367        // `L"x"` is one token rather than an identifier followed by a string, so the prefixes
368        // are checked before the identifier scan rather than unwound afterwards.
369        let (n1, n2) = (self.cursor.nth(1), self.cursor.nth(2));
370        match b {
371            b'L' | b'u' | b'U' if n1 == b'"' => {
372                self.eat();
373                self.literal(b'"', start, PpTokenKind::StringLit)
374            }
375            b'L' | b'u' | b'U' if n1 == b'\'' => {
376                self.eat();
377                self.literal(b'\'', start, PpTokenKind::CharConst)
378            }
379            // `u8"s"` is C11. `u8'c'` is C23.
380            b'u' if n1 == b'8' && (n2 == b'"' || n2 == b'\'') => {
381                self.eat();
382                self.eat();
383                let kind = if n2 == b'"' { PpTokenKind::StringLit } else { PpTokenKind::CharConst };
384                self.literal(n2, start, kind)
385            }
386            _ => self.identifier(),
387        }
388    }
389
390    fn identifier(&mut self) -> PpTokenKind {
391        while !self.cursor.at_end() {
392            let b = self.cursor.first();
393            if is_ident_continue(b) {
394                self.eat();
395            } else if b == b'\\' && matches!(self.cursor.nth(1), b'u' | b'U') {
396                // A universal character name spells an identifier character. Whether the
397                // character it names is allowed in an identifier is a phase 7 question,
398                // because the answer depends on `-std=`.
399                self.eat();
400                self.eat();
401            } else {
402                break;
403            }
404        }
405        PpTokenKind::Ident
406    }
407
408    fn pp_number(&mut self) -> PpTokenKind {
409        // The pp-number grammar is deliberately looser than the constant grammar, so `1.2.3`
410        // and `0x1p+3` are both one token here. Rejecting the first belongs to phase 7, and
411        // doing it here would break `##` pasting that builds a number out of pieces.
412        self.eat();
413        while !self.cursor.at_end() {
414            let b = self.cursor.first();
415            let n1 = self.cursor.nth(1);
416            if matches!(b, b'e' | b'E' | b'p' | b'P') && matches!(n1, b'+' | b'-') {
417                self.eat();
418                self.eat();
419            } else if is_ident_continue(b) || b == b'.' {
420                self.eat();
421            } else if b == b'\'' && is_ident_continue(n1) {
422                // C23 digit separators. `1'000'000` is one pp-number, and the apostrophe only
423                // separates when an identifier character follows, so `1'a'` still ends the
424                // number where a character constant begins.
425                self.eat();
426                self.eat();
427            } else if b == b'\\' && matches!(n1, b'u' | b'U') {
428                self.eat();
429                self.eat();
430            } else {
431                break;
432            }
433        }
434        PpTokenKind::Number
435    }
436
437    fn literal(&mut self, quote: u8, start: u32, kind: PpTokenKind) -> PpTokenKind {
438        self.eat();
439        loop {
440            if self.cursor.at_end() || self.cursor.first() == b'\n' {
441                // A literal does not cross a line. Reporting it here and stopping at the
442                // newline is what keeps one missing quote from swallowing the rest of the
443                // file and turning into a hundred nonsense errors.
444                let span = Span::new(self.file_start + start, self.file_start + self.cursor.pos());
445                let what = if quote == b'"' { "string literal" } else { "character constant" };
446                self.diagnostics
447                    .push(Diagnostic::error(format!("missing terminating quote in {what}"), span));
448                break;
449            }
450            let b = self.eat();
451            if b == quote {
452                break;
453            }
454            if b == b'\\' && !self.cursor.at_end() && self.cursor.first() != b'\n' {
455                // What the escape means is phase 5's problem. All this needs to know is that
456                // the next byte cannot end the literal.
457                self.eat();
458            }
459        }
460        kind
461    }
462
463    /// Scans a punctuator, or returns `None` without consuming anything when the byte begins
464    /// no punctuator at all.
465    fn punctuator(&mut self, start: u32, flags: TokenFlags) -> Option<PpToken> {
466        let (punct, len, digraph) = self.punctuator_kind()?;
467        for _ in 0..len {
468            self.eat();
469        }
470        let end = self.cursor.pos();
471        let mut flags = flags;
472        if digraph {
473            flags = flags.with(TokenFlags::DIGRAPH);
474        }
475        if self.unclean {
476            flags = flags.with(TokenFlags::SPLICED);
477        }
478        let span = Span::new(self.file_start + start, self.file_start + end);
479        Some(PpToken { kind: PpTokenKind::Punct(punct), flags, value: None, span })
480    }
481
482    /// Longest match over the punctuator set, measured in logical bytes.
483    fn punctuator_kind(&self) -> Option<(Punct, usize, bool)> {
484        let one = self.cursor.first();
485        let two = self.cursor.nth(1);
486        let three = self.cursor.nth(2);
487        let four = self.cursor.nth(3);
488        let found = match one {
489            b'[' => (Punct::LBracket, 1, false),
490            b']' => (Punct::RBracket, 1, false),
491            b'(' => (Punct::LParen, 1, false),
492            b')' => (Punct::RParen, 1, false),
493            b'{' => (Punct::LBrace, 1, false),
494            b'}' => (Punct::RBrace, 1, false),
495            b'~' => (Punct::Tilde, 1, false),
496            b'?' => (Punct::Question, 1, false),
497            b';' => (Punct::Semi, 1, false),
498            b',' => (Punct::Comma, 1, false),
499            b'.' if two == b'.' && three == b'.' => (Punct::Ellipsis, 3, false),
500            b'.' => (Punct::Dot, 1, false),
501            b'-' => match two {
502                b'>' => (Punct::Arrow, 2, false),
503                b'-' => (Punct::MinusMinus, 2, false),
504                b'=' => (Punct::MinusEq, 2, false),
505                _ => (Punct::Minus, 1, false),
506            },
507            b'+' => match two {
508                b'+' => (Punct::PlusPlus, 2, false),
509                b'=' => (Punct::PlusEq, 2, false),
510                _ => (Punct::Plus, 1, false),
511            },
512            b'&' => match two {
513                b'&' => (Punct::AmpAmp, 2, false),
514                b'=' => (Punct::AmpEq, 2, false),
515                _ => (Punct::Amp, 1, false),
516            },
517            b'|' => match two {
518                b'|' => (Punct::PipePipe, 2, false),
519                b'=' => (Punct::PipeEq, 2, false),
520                _ => (Punct::Pipe, 1, false),
521            },
522            b'*' if two == b'=' => (Punct::StarEq, 2, false),
523            b'*' => (Punct::Star, 1, false),
524            b'/' if two == b'=' => (Punct::SlashEq, 2, false),
525            b'/' => (Punct::Slash, 1, false),
526            b'!' if two == b'=' => (Punct::Ne, 2, false),
527            b'!' => (Punct::Bang, 1, false),
528            b'^' if two == b'=' => (Punct::CaretEq, 2, false),
529            b'^' => (Punct::Caret, 1, false),
530            b'=' if two == b'=' => (Punct::EqEq, 2, false),
531            b'=' => (Punct::Eq, 1, false),
532            b':' => match two {
533                b'>' => (Punct::RBracket, 2, true),
534                b':' => (Punct::ColonColon, 2, false),
535                _ => (Punct::Colon, 1, false),
536            },
537            b'<' => match two {
538                b'<' if three == b'=' => (Punct::ShlEq, 3, false),
539                b'<' => (Punct::Shl, 2, false),
540                b'=' => (Punct::Le, 2, false),
541                b':' => (Punct::LBracket, 2, true),
542                b'%' => (Punct::LBrace, 2, true),
543                _ => (Punct::Lt, 1, false),
544            },
545            b'>' => match two {
546                b'>' if three == b'=' => (Punct::ShrEq, 3, false),
547                b'>' => (Punct::Shr, 2, false),
548                b'=' => (Punct::Ge, 2, false),
549                _ => (Punct::Gt, 1, false),
550            },
551            b'%' => match two {
552                b'=' => (Punct::PercentEq, 2, false),
553                b'>' => (Punct::RBrace, 2, true),
554                b':' if three == b'%' && four == b':' => (Punct::HashHash, 4, true),
555                b':' => (Punct::Hash, 2, true),
556                _ => (Punct::Percent, 1, false),
557            },
558            b'#' if two == b'#' => (Punct::HashHash, 2, false),
559            b'#' => (Punct::Hash, 1, false),
560            _ => return None,
561        };
562        Some(found)
563    }
564}
565
566/// Scans `src` to the end and returns every preprocessing token and every complaint.
567///
568/// The end of file token is included, because everything downstream wants somewhere to point
569/// when a construct runs off the end of the file.
570pub fn tokenize(
571    src: &[u8],
572    file_start: BytePos,
573    opts: Options,
574    interner: &mut Interner,
575) -> (Vec<PpToken>, Vec<Diagnostic>) {
576    let mut lexer = Lexer::new(src, file_start, opts);
577    let mut out = Vec::new();
578    loop {
579        let token = lexer.next_token(interner);
580        let done = token.is_eof();
581        out.push(token);
582        if done {
583            break;
584        }
585    }
586    let diagnostics = lexer.take_diagnostics();
587    (out, diagnostics)
588}