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