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}
46
47impl<'a> Context<'a> {
48 /// A context with GCC's include depth limit.
49 pub fn new(
50 interner: &'a mut Interner,
51 sources: &'a mut SourceMap,
52 fs: &'a dyn FileSystem,
53 search: &'a SearchPath,
54 ) -> Context<'a> {
55 Context { interner, sources, fs, search, lex: Options::new(), max_include_depth: 200 }
56 }
57}
58
59impl std::fmt::Debug for Context<'_> {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 f.debug_struct("Context")
62 .field("lex", &self.lex)
63 .field("max_include_depth", &self.max_include_depth)
64 .finish_non_exhaustive()
65 }
66}
67
68/// One open file, and what an `#include` written in it resolves against.
69#[derive(Debug)]
70pub(crate) struct Frame {
71 /// Where the file was included from, for the too deeply nested diagnostic.
72 pub(crate) at: Span,
73 /// The file itself, which is what `#pragma once` and the guard optimization remember it
74 /// by. A path rather than a device and inode pair, so two names for one file are two
75 /// files here, which is what a file system abstraction with no `stat` in it can say.
76 pub(crate) path: PathBuf,
77 /// The directory the file is in, which a quoted include looks in first.
78 pub(crate) dir: Option<PathBuf>,
79 /// Where an `#include_next` written in this file starts looking.
80 pub(crate) next: usize,
81}
82
83/// Pulls tokens out of the lexer one logical line at a time.
84///
85/// One token of lookahead, because a line ends when the next token says it starts a line and
86/// there is no other way to find that out.
87pub(crate) struct Reader<'a> {
88 lexer: Lexer<'a>,
89 pending: Option<PpToken>,
90}
91
92impl<'a> Reader<'a> {
93 pub(crate) fn new(src: &'a [u8], start: BytePos, opts: Options) -> Reader<'a> {
94 Reader { lexer: Lexer::new(src, start, opts), pending: None }
95 }
96
97 /// The next token, which is an end of file token forever once the file runs out.
98 pub(crate) fn next(&mut self, interner: &mut Interner) -> PpToken {
99 match self.pending.take() {
100 Some(token) => token,
101 None => self.lexer.next_token(interner),
102 }
103 }
104
105 /// Puts a token back, so the next call to [`Reader::next`] returns it again.
106 pub(crate) fn put_back(&mut self, token: PpToken) {
107 self.pending = Some(token);
108 }
109
110 /// Appends the rest of the current line to `out`, leaving the next line's first token
111 /// where the next call will find it.
112 pub(crate) fn line(&mut self, interner: &mut Interner, out: &mut Vec<PpToken>) {
113 loop {
114 let token = self.next(interner);
115 if token.is_eof() || token.flags.has(TokenFlags::START_OF_LINE) {
116 self.put_back(token);
117 return;
118 }
119 out.push(token);
120 }
121 }
122
123 /// Scans a header name here, which only an include directive may ask for.
124 ///
125 /// `None` when the line does not begin with `<` or `"`, which is the computed include
126 /// case and has to be answered by macro expansion instead.
127 pub(crate) fn header_name(&mut self, interner: &mut Interner) -> Option<PpToken> {
128 // Asking after the line has been read would scan the wrong bytes, and the borrow
129 // checker cannot see the difference, so the invariant is stated here instead.
130 debug_assert!(self.pending.is_none(), "the header name has to be asked for first");
131 self.lexer.header_name(interner)
132 }
133
134 pub(crate) fn take_diagnostics(&mut self) -> Vec<Diagnostic> {
135 self.lexer.take_diagnostics()
136 }
137}
138
139/// What the two spellings of a header name mean, and the name itself.
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub(crate) struct Header {
142 pub(crate) name: String,
143 pub(crate) angled: bool,
144}
145
146/// Reads a header name out of the token the lexer produced for one.
147///
148/// The delimiters come off and nothing else happens: a header name is not a string literal,
149/// so a backslash in it is a backslash and `\t` names a file whose name contains a `t`
150/// preceded by a backslash, which is what a Windows path needs.
151pub(crate) fn header_from_token(spelling: &str) -> Option<Header> {
152 let angled = spelling.starts_with('<');
153 let close = if angled { '>' } else { '"' };
154 let inner = spelling.strip_prefix(if angled { '<' } else { '"' })?;
155 let inner = inner.strip_suffix(close).unwrap_or(inner);
156 if inner.is_empty() {
157 return None;
158 }
159 Some(Header { name: inner.to_owned(), angled })
160}
161
162/// Reads a header name out of the tokens a macro expanded to.
163///
164/// `#include MACRO` is the computed include, and the standard says only that the tokens are
165/// combined in an implementation defined manner. The manner is that spellings are
166/// concatenated with nothing between them, which is what GCC does in every case that occurs
167/// in real code and is the only choice that makes `<sys/types.h>` come back out as itself.
168pub(crate) fn header_from_tokens(spellings: &[&str]) -> Option<Header> {
169 let first = *spellings.first()?;
170 if first.starts_with('"') && spellings.len() == 1 {
171 return header_from_token(first);
172 }
173 if first != "<" {
174 return None;
175 }
176 let close = spellings.iter().rposition(|s| *s == ">")?;
177 if close < 2 {
178 return None;
179 }
180 let name: String = spellings[1..close].concat();
181 Some(Header { name, angled: true })
182}
183
184/// What a file name is called when the position it was asked about is in no file at all.
185///
186/// The same spelling the diagnostic renderer uses, so there is one word for this and not two.
187pub(crate) const UNKNOWN: &str = "<unknown>";
188
189/// A file name as the string literal `__FILE__` expands to.
190///
191/// A backslash and a double quote are escaped. That is not a nicety on Windows: `__FILE__` for
192/// `C:\src\a.c` has to be a literal that means that path, and leaving the backslashes alone
193/// would produce `\s` and `\a`, one of which is an unknown escape and the other of which is a
194/// bell character.
195pub(crate) fn quoted(name: &str) -> String {
196 let mut out = String::with_capacity(name.len() + 2);
197 out.push('"');
198 for ch in name.chars() {
199 if ch == '\\' || ch == '"' {
200 out.push('\\');
201 }
202 out.push(ch);
203 }
204 out.push('"');
205 out
206}
207
208/// The last component of a path, which is what `__FILE_NAME__` is.
209///
210/// Both separators are cut rather than the platform's own, because a header included as
211/// `sys/types.h` on Windows is found at a path with one of each in it.
212pub(crate) fn base_name(name: &str) -> &str {
213 match name.rfind(['/', '\\']) {
214 Some(at) => &name[at + 1..],
215 None => name,
216 }
217}
218
219/// The directory a file is in, for a quoted include written inside it.
220pub(crate) fn directory_of(name: &str) -> Option<PathBuf> {
221 Path::new(name).parent().map(Path::to_path_buf)
222}
223
224/// The spelling of a token, for the computed include path.
225pub(crate) fn spelling(token: Tok, interner: &Interner) -> &str {
226 match token.kind {
227 PpTokenKind::Punct(p) => p.as_str(),
228 _ => token.value.map_or("", |v| interner.resolve(v)),
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn a_header_name_keeps_everything_between_the_delimiters() {
238 assert_eq!(
239 header_from_token("<sys/types.h>"),
240 Some(Header { name: "sys/types.h".to_owned(), angled: true })
241 );
242 assert_eq!(
243 header_from_token("\"local.h\""),
244 Some(Header { name: "local.h".to_owned(), angled: false })
245 );
246 }
247
248 #[test]
249 fn a_backslash_in_a_header_name_is_a_backslash() {
250 // Not an escape. A header name is not a string literal, and `\t` here names a file.
251 let header = header_from_token("\"win32\\types.h\"").unwrap();
252 assert_eq!(header.name, "win32\\types.h");
253 }
254
255 #[test]
256 fn an_empty_header_name_is_not_a_header_name() {
257 assert_eq!(header_from_token("<>"), None);
258 assert_eq!(header_from_token("\"\""), None);
259 }
260
261 #[test]
262 fn a_computed_include_concatenates_the_spellings() {
263 let header = header_from_tokens(&["<", "sys", "/", "types", ".", "h", ">"]).unwrap();
264 assert_eq!(header.name, "sys/types.h");
265 assert!(header.angled);
266 }
267
268 #[test]
269 fn a_computed_include_can_expand_to_a_string_literal() {
270 let header = header_from_tokens(&["\"local.h\""]).unwrap();
271 assert_eq!(header.name, "local.h");
272 assert!(!header.angled);
273 }
274
275 #[test]
276 fn a_computed_include_that_is_neither_is_refused() {
277 assert_eq!(header_from_tokens(&[]), None);
278 assert_eq!(header_from_tokens(&["1"]), None);
279 assert_eq!(header_from_tokens(&["<", "a"]), None);
280 assert_eq!(header_from_tokens(&["<", ">"]), None);
281 }
282
283 #[test]
284 fn the_last_angle_bracket_closes_the_name() {
285 // `<a>b>` is not something anyone writes on purpose, but taking the first `>` would
286 // silently drop the rest, and taking the last one at least round trips.
287 let header = header_from_tokens(&["<", "a", ">", "b", ">"]).unwrap();
288 assert_eq!(header.name, "a>b");
289 }
290}