Skip to main content

rucc_pp/
include.rs

1//! Reading a file, and the parts of `#include` that are not the directive itself.
2//!
3//! Design: `spec/05-preprocessor.md` section 5.4 and `spec/04-driver-and-cli.md` section 4.4.
4//!
5//! Phase 4 drives the lexer rather than being handed a finished token vector, and the reason
6//! is header names. `<stdio.h>` and a run of comparisons are the same bytes, and only the
7//! directive knows which one is possible, so [`rucc_lex::Lexer::header_name`] exists and has
8//! to be called at exactly the right moment. Scanning the file first and reconstructing the
9//! name from the tokens afterwards works until a header name contains `//`, or a backslash on
10//! a Windows path, and then it silently produces a different name.
11//!
12//! Driving the lexer also means a file is read one line at a time rather than all at once,
13//! which is what the memory mapped input and the header cache both want later.
14
15use std::path::{Path, PathBuf};
16
17use rucc_base::Interner;
18use rucc_diag::{BytePos, Diagnostic, SourceMap, Span};
19use rucc_lex::{Lexer, Options, PpToken, PpTokenKind, TokenFlags};
20use rucc_session::{FileSystem, SearchPath};
21
22use crate::token::Tok;
23
24/// Everything phase 4 needs from outside itself.
25///
26/// Grouped into one struct because `#include` needs all of it at once and threading five
27/// references through every directive handler is how a parameter list becomes unreadable.
28/// The lifetime is the compilation, and every field of it lives on the session.
29pub struct Context<'a> {
30    /// The one interner.
31    pub interner: &'a mut Interner,
32    /// Where an included file is added, and what a span is resolved against.
33    pub sources: &'a mut SourceMap,
34    /// Where a header is read from.
35    pub fs: &'a dyn FileSystem,
36    /// Where a header is looked for.
37    pub search: &'a SearchPath,
38    /// The dialect knobs phase 1 cares about.
39    pub lex: Options,
40    /// How deep `#include` may nest before it is called a cycle.
41    ///
42    /// A header that includes itself with no guard is the common way to reach this, and the
43    /// alternative to a limit is a stack overflow with no diagnostic at all.
44    pub max_include_depth: u32,
45    /// Whether `-Wpedantic` is on, which is the only thing phase 4 asks it about today.
46    ///
47    /// Here rather than on the preprocessor because it is a fact about the command line and the
48    /// command line is what builds this. The parser keeps the same flag in the same place for
49    /// the same reason.
50    pub pedantic: bool,
51}
52
53impl<'a> Context<'a> {
54    /// A context with GCC's include depth limit.
55    pub fn new(
56        interner: &'a mut Interner,
57        sources: &'a mut SourceMap,
58        fs: &'a dyn FileSystem,
59        search: &'a SearchPath,
60    ) -> Context<'a> {
61        Context {
62            interner,
63            sources,
64            fs,
65            search,
66            lex: Options::new(),
67            max_include_depth: 200,
68            pedantic: false,
69        }
70    }
71}
72
73impl std::fmt::Debug for Context<'_> {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        f.debug_struct("Context")
76            .field("lex", &self.lex)
77            .field("max_include_depth", &self.max_include_depth)
78            .finish_non_exhaustive()
79    }
80}
81
82/// One open file, and what an `#include` written in it resolves against.
83#[derive(Debug)]
84pub(crate) struct Frame {
85    /// Where the file was included from, for the too deeply nested diagnostic.
86    pub(crate) at: Span,
87    /// The file itself, as the include that found it named it. This is what diagnostics and
88    /// `#include_next` are written against, so it stays the name that was used rather than
89    /// the name the file system would rather it had.
90    pub(crate) path: PathBuf,
91    /// What the file system calls the same file, which is what `#pragma once` and the guard
92    /// optimization remember it by. Two names for one file share this and do not share the
93    /// path, and reaching one header through two spellings is ordinary on any project with
94    /// more than one include directory.
95    pub(crate) id: PathBuf,
96    /// The directory the file is in, which a quoted include looks in first.
97    pub(crate) dir: Option<PathBuf>,
98    /// Where an `#include_next` written in this file starts looking.
99    pub(crate) next: usize,
100}
101
102/// Pulls tokens out of the lexer one logical line at a time.
103///
104/// One token of lookahead, because a line ends when the next token says it starts a line and
105/// there is no other way to find that out.
106pub(crate) struct Reader<'a> {
107    lexer: Lexer<'a>,
108    pending: Option<PpToken>,
109}
110
111impl<'a> Reader<'a> {
112    pub(crate) fn new(src: &'a [u8], start: BytePos, opts: Options) -> Reader<'a> {
113        Reader { lexer: Lexer::new(src, start, opts), pending: None }
114    }
115
116    /// The next token, which is an end of file token forever once the file runs out.
117    pub(crate) fn next(&mut self, interner: &mut Interner) -> PpToken {
118        match self.pending.take() {
119            Some(token) => token,
120            None => self.lexer.next_token(interner),
121        }
122    }
123
124    /// Puts a token back, so the next call to [`Reader::next`] returns it again.
125    pub(crate) fn put_back(&mut self, token: PpToken) {
126        self.pending = Some(token);
127    }
128
129    /// Appends the rest of the current line to `out`, leaving the next line's first token
130    /// where the next call will find it.
131    pub(crate) fn line(&mut self, interner: &mut Interner, out: &mut Vec<PpToken>) {
132        loop {
133            let token = self.next(interner);
134            if token.is_eof() || token.flags.has(TokenFlags::START_OF_LINE) {
135                self.put_back(token);
136                return;
137            }
138            out.push(token);
139        }
140    }
141
142    /// Scans a header name here, which only an include directive may ask for.
143    ///
144    /// `None` when the line does not begin with `<` or `"`, which is the computed include
145    /// case and has to be answered by macro expansion instead.
146    pub(crate) fn header_name(&mut self, interner: &mut Interner) -> Option<PpToken> {
147        // Asking after the line has been read would scan the wrong bytes, and the borrow
148        // checker cannot see the difference, so the invariant is stated here instead.
149        debug_assert!(self.pending.is_none(), "the header name has to be asked for first");
150        self.lexer.header_name(interner)
151    }
152
153    pub(crate) fn take_diagnostics(&mut self) -> Vec<Diagnostic> {
154        self.lexer.take_diagnostics()
155    }
156}
157
158/// One file an `#include` found, for the `-M` family.
159///
160/// The path is the one the search resolved to rather than the name the directive wrote, because
161/// a make rule naming `stdio.h` would say nothing about which `stdio.h`, and it is left relative
162/// where the directory it was found under was relative, which is what makes the rule readable
163/// and what GCC does.
164#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct Dependency {
166    /// Where the file was found.
167    pub path: PathBuf,
168    /// Whether the directory it was found under is a system one, which is what `-MM` drops.
169    pub is_system: bool,
170}
171
172/// What the two spellings of a header name mean, and the name itself.
173#[derive(Debug, Clone, PartialEq, Eq)]
174pub(crate) struct Header {
175    pub(crate) name: String,
176    pub(crate) angled: bool,
177}
178
179/// Reads a header name out of the token the lexer produced for one.
180///
181/// The delimiters come off and nothing else happens: a header name is not a string literal,
182/// so a backslash in it is a backslash and `\t` names a file whose name contains a `t`
183/// preceded by a backslash, which is what a Windows path needs.
184pub(crate) fn header_from_token(spelling: &str) -> Option<Header> {
185    let angled = spelling.starts_with('<');
186    let close = if angled { '>' } else { '"' };
187    let inner = spelling.strip_prefix(if angled { '<' } else { '"' })?;
188    let inner = inner.strip_suffix(close).unwrap_or(inner);
189    if inner.is_empty() {
190        return None;
191    }
192    Some(Header { name: inner.to_owned(), angled })
193}
194
195/// Reads a header name out of the tokens a macro expanded to.
196///
197/// `#include MACRO` is the computed include, and the standard says only that the tokens are
198/// combined in an implementation defined manner. The manner is that spellings are
199/// concatenated with nothing between them, which is what GCC does in every case that occurs
200/// in real code and is the only choice that makes `<sys/types.h>` come back out as itself.
201pub(crate) fn header_from_tokens(spellings: &[&str]) -> Option<Header> {
202    let first = *spellings.first()?;
203    if first.starts_with('"') && spellings.len() == 1 {
204        return header_from_token(first);
205    }
206    if first != "<" {
207        return None;
208    }
209    let close = spellings.iter().rposition(|s| *s == ">")?;
210    if close < 2 {
211        return None;
212    }
213    let name: String = spellings[1..close].concat();
214    Some(Header { name, angled: true })
215}
216
217/// What a file name is called when the position it was asked about is in no file at all.
218///
219/// The same spelling the diagnostic renderer uses, so there is one word for this and not two.
220pub(crate) const UNKNOWN: &str = "<unknown>";
221
222/// A file name as the string literal `__FILE__` expands to.
223///
224/// A backslash and a double quote are escaped. That is not a nicety on Windows: `__FILE__` for
225/// `C:\src\a.c` has to be a literal that means that path, and leaving the backslashes alone
226/// would produce `\s` and `\a`, one of which is an unknown escape and the other of which is a
227/// bell character.
228pub(crate) fn quoted(name: &str) -> String {
229    let mut out = String::with_capacity(name.len() + 2);
230    out.push('"');
231    for ch in name.chars() {
232        if ch == '\\' || ch == '"' {
233            out.push('\\');
234        }
235        out.push(ch);
236    }
237    out.push('"');
238    out
239}
240
241/// The last component of a path, which is what `__FILE_NAME__` is.
242///
243/// Both separators are cut rather than the platform's own, because a header included as
244/// `sys/types.h` on Windows is found at a path with one of each in it.
245pub(crate) fn base_name(name: &str) -> &str {
246    match name.rfind(['/', '\\']) {
247        Some(at) => &name[at + 1..],
248        None => name,
249    }
250}
251
252/// The directory a file is in, for a quoted include written inside it.
253pub(crate) fn directory_of(name: &str) -> Option<PathBuf> {
254    Path::new(name).parent().map(Path::to_path_buf)
255}
256
257/// The spelling of a token, for the computed include path.
258pub(crate) fn spelling(token: Tok, interner: &Interner) -> &str {
259    match token.kind {
260        PpTokenKind::Punct(p) => p.as_str(),
261        _ => token.value.map_or("", |v| interner.resolve(v)),
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268
269    #[test]
270    fn a_header_name_keeps_everything_between_the_delimiters() {
271        assert_eq!(
272            header_from_token("<sys/types.h>"),
273            Some(Header { name: "sys/types.h".to_owned(), angled: true })
274        );
275        assert_eq!(
276            header_from_token("\"local.h\""),
277            Some(Header { name: "local.h".to_owned(), angled: false })
278        );
279    }
280
281    #[test]
282    fn a_backslash_in_a_header_name_is_a_backslash() {
283        // Not an escape. A header name is not a string literal, and `\t` here names a file.
284        let header = header_from_token("\"win32\\types.h\"").unwrap();
285        assert_eq!(header.name, "win32\\types.h");
286    }
287
288    #[test]
289    fn an_empty_header_name_is_not_a_header_name() {
290        assert_eq!(header_from_token("<>"), None);
291        assert_eq!(header_from_token("\"\""), None);
292    }
293
294    #[test]
295    fn a_computed_include_concatenates_the_spellings() {
296        let header = header_from_tokens(&["<", "sys", "/", "types", ".", "h", ">"]).unwrap();
297        assert_eq!(header.name, "sys/types.h");
298        assert!(header.angled);
299    }
300
301    #[test]
302    fn a_computed_include_can_expand_to_a_string_literal() {
303        let header = header_from_tokens(&["\"local.h\""]).unwrap();
304        assert_eq!(header.name, "local.h");
305        assert!(!header.angled);
306    }
307
308    #[test]
309    fn a_computed_include_that_is_neither_is_refused() {
310        assert_eq!(header_from_tokens(&[]), None);
311        assert_eq!(header_from_tokens(&["1"]), None);
312        assert_eq!(header_from_tokens(&["<", "a"]), None);
313        assert_eq!(header_from_tokens(&["<", ">"]), None);
314    }
315
316    #[test]
317    fn the_last_angle_bracket_closes_the_name() {
318        // `<a>b>` is not something anyone writes on purpose, but taking the first `>` would
319        // silently drop the rest, and taking the last one at least round trips.
320        let header = header_from_tokens(&["<", "a", ">", "b", ">"]).unwrap();
321        assert_eq!(header.name, "a>b");
322    }
323}