Skip to main content

rucc_session/
fs.rs

1//! The file system the compiler reads through, and the include search path.
2//!
3//! Design: `spec/04-driver-and-cli.md` section 4.4 for the search order, and
4//! `spec/05-preprocessor.md` section 5.4 for what `#include_next` means.
5//!
6//! Nothing below the driver calls `std::fs`. That is what makes the compiler usable as a
7//! library, and it is what makes a preprocessor test a value rather than a temporary
8//! directory: [`MemoryFileSystem`] is a map from path to bytes, and a test that needs a
9//! twelve deep header nest builds one in twelve lines with no clean up to forget.
10//!
11//! ```
12//! use rucc_session::{FileSystem, IncludeForm, MemoryFileSystem, SearchPath};
13//!
14//! let mut fs = MemoryFileSystem::new();
15//! fs.insert("/usr/include/stdio.h", *b"int puts(const char *);\n");
16//!
17//! let mut search = SearchPath::new();
18//! search.push_system("/usr/include");
19//!
20//! let found = search.resolve(&fs, "stdio.h", IncludeForm::Angled, None, 0).unwrap();
21//! assert_eq!(found.name.replace('\\', "/"), "/usr/include/stdio.h");
22//! assert!(found.is_system);
23//! ```
24
25use std::collections::BTreeMap;
26use std::fmt;
27use std::io;
28use std::path::{Path, PathBuf};
29
30use rucc_diag::SourceBytes;
31
32/// Where the compiler reads source from.
33///
34/// The one method is deliberate. Everything the preprocessor wants to know about a file, up
35/// to and including whether it exists, is answered by trying to read it, and an interface
36/// with a separate `exists` invites the race where the answer changes between the two calls.
37pub trait FileSystem: fmt::Debug + Send + Sync {
38    /// Reads a file.
39    ///
40    /// # Errors
41    ///
42    /// Whatever the underlying file system says. [`io::ErrorKind::NotFound`] is the ordinary
43    /// case during an include search and is not by itself a problem.
44    fn read(&self, path: &Path) -> io::Result<SourceBytes>;
45}
46
47/// A file system held in memory, for tests and for embedding the compiler.
48#[derive(Debug, Default)]
49pub struct MemoryFileSystem {
50    files: BTreeMap<PathBuf, SourceBytes>,
51}
52
53impl MemoryFileSystem {
54    /// An empty file system.
55    pub fn new() -> MemoryFileSystem {
56        MemoryFileSystem::default()
57    }
58
59    /// Adds a file, replacing any file already at that path.
60    pub fn insert(
61        &mut self,
62        path: impl Into<PathBuf>,
63        contents: impl AsRef<[u8]> + Send + Sync + 'static,
64    ) {
65        self.files.insert(path.into(), SourceBytes::new(contents));
66    }
67
68    /// How many files it holds.
69    pub fn len(&self) -> usize {
70        self.files.len()
71    }
72
73    /// Whether it holds nothing.
74    pub fn is_empty(&self) -> bool {
75        self.files.is_empty()
76    }
77}
78
79impl FileSystem for MemoryFileSystem {
80    fn read(&self, path: &Path) -> io::Result<SourceBytes> {
81        self.files
82            .get(path)
83            .cloned()
84            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no such file"))
85    }
86}
87
88/// Which spelling an `#include` used.
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
90pub enum IncludeForm {
91    /// `#include "local.h"`, which looks next to the including file first.
92    Quoted,
93    /// `#include <stdio.h>`, which does not.
94    Angled,
95}
96
97/// One directory on the search path.
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct Dir {
100    /// The directory, as the user spelled it. Not canonicalised, because a diagnostic that
101    /// says `../include/foo.h` is more use than one naming a path the user never typed.
102    pub path: PathBuf,
103    /// Whether headers found here are system headers, which suppresses warnings in them and
104    /// sets the `3` flag on a `-E` line marker.
105    pub is_system: bool,
106}
107
108/// A header that was found.
109#[derive(Debug, Clone)]
110pub struct Found {
111    /// The path to open, which is the directory joined with the name as written.
112    pub path: PathBuf,
113    /// That path as a string, for the source map and for diagnostics.
114    pub name: String,
115    /// Whether it came from a system directory.
116    pub is_system: bool,
117    /// Where an `#include_next` written in this file should start looking.
118    ///
119    /// One past the entry this header came from, or zero for a header found next to the file
120    /// that included it, because that directory is not on the path and there is nothing to
121    /// continue past. Carrying the answer rather than the position is what keeps the two
122    /// cases from being confused at the call site.
123    pub next: usize,
124    /// The contents.
125    pub bytes: SourceBytes,
126}
127
128/// The directories a header is looked for in, in order.
129///
130/// GCC's order, because a different one produces header shadowing bugs that are miserable to
131/// diagnose: `-iquote` first and only for a quoted include, then `-I`, then `-isystem`, then
132/// the configured system directories, then `-idirafter`. The directory of the including file
133/// comes before all of it for a quoted include, and it is not part of the numbered list
134/// because `#include_next` must not be able to land back on it.
135#[derive(Debug, Default, Clone, PartialEq, Eq)]
136pub struct SearchPath {
137    dirs: Vec<Dir>,
138    /// Where the `-I` directories begin, which is where an angled include starts looking.
139    quote_end: usize,
140    /// Where the `-isystem` and configured system directories begin.
141    bracket_end: usize,
142    /// Where the `-idirafter` directories begin.
143    system_end: usize,
144}
145
146impl SearchPath {
147    /// An empty search path.
148    pub fn new() -> SearchPath {
149        SearchPath::default()
150    }
151
152    /// Adds a `-iquote` directory, searched only for a quoted include.
153    pub fn push_quote(&mut self, dir: impl Into<PathBuf>) {
154        let at = self.quote_end;
155        self.insert(at, dir.into(), false);
156        self.quote_end += 1;
157        self.bracket_end += 1;
158        self.system_end += 1;
159    }
160
161    /// Adds a `-I` directory.
162    pub fn push_bracket(&mut self, dir: impl Into<PathBuf>) {
163        let at = self.bracket_end;
164        self.insert(at, dir.into(), false);
165        self.bracket_end += 1;
166        self.system_end += 1;
167    }
168
169    /// Adds a `-isystem` directory, or one of the target's configured system directories.
170    pub fn push_system(&mut self, dir: impl Into<PathBuf>) {
171        let at = self.system_end;
172        self.insert(at, dir.into(), true);
173        self.system_end += 1;
174    }
175
176    /// Adds a `-idirafter` directory, which is searched after everything else.
177    pub fn push_after(&mut self, dir: impl Into<PathBuf>) {
178        let at = self.dirs.len();
179        self.insert(at, dir.into(), true);
180    }
181
182    fn insert(&mut self, at: usize, path: PathBuf, is_system: bool) {
183        self.dirs.insert(at, Dir { path, is_system });
184    }
185
186    /// Every directory, in search order.
187    pub fn dirs(&self) -> &[Dir] {
188        &self.dirs
189    }
190
191    /// The first entry an include of this form looks at.
192    ///
193    /// An angled include skips the `-iquote` directories, which is the only difference
194    /// between the two chains once the including file's own directory is out of the way.
195    pub fn start(&self, form: IncludeForm) -> usize {
196        match form {
197            IncludeForm::Quoted => 0,
198            IncludeForm::Angled => self.quote_end,
199        }
200    }
201
202    /// Finds `name`, starting at entry `from` of the search path.
203    ///
204    /// `relative_to` is the directory of the file doing the including, tried first for a
205    /// quoted include and ignored otherwise. Pass `None` for an `#include_next`, which is
206    /// defined as continuing past the directory the current file was found in and so must not
207    /// look next to it again.
208    ///
209    /// An absolute name is opened directly and the search path is not consulted, which is
210    /// what every C compiler does and what a generated header with an absolute path needs.
211    pub fn resolve(
212        &self,
213        fs: &dyn FileSystem,
214        name: &str,
215        form: IncludeForm,
216        relative_to: Option<&Path>,
217        from: usize,
218    ) -> Option<Found> {
219        let as_path = Path::new(name);
220        if is_absolute(as_path) {
221            let bytes = fs.read(as_path).ok()?;
222            return Some(Found {
223                path: as_path.to_path_buf(),
224                name: name.to_owned(),
225                is_system: false,
226                next: 0,
227                bytes,
228            });
229        }
230        if form == IncludeForm::Quoted {
231            if let Some(dir) = relative_to {
232                let path = dir.join(as_path);
233                if let Ok(bytes) = fs.read(&path) {
234                    return Some(Found {
235                        name: display(&path),
236                        path,
237                        is_system: false,
238                        // The including file's own directory is not an entry on the path, so
239                        // an `#include_next` from a header found there starts at the top of
240                        // the path rather than one past a position that does not exist.
241                        next: 0,
242                        bytes,
243                    });
244                }
245            }
246        }
247        for (at, dir) in self.dirs.iter().enumerate().skip(from) {
248            let path = dir.path.join(as_path);
249            if let Ok(bytes) = fs.read(&path) {
250                return Some(Found {
251                    name: display(&path),
252                    path,
253                    is_system: dir.is_system,
254                    next: at + 1,
255                    bytes,
256                });
257            }
258        }
259        None
260    }
261
262    /// The directories a failed [`SearchPath::resolve`] with the same arguments looked in.
263    ///
264    /// `spec/05-preprocessor.md` section 5.7 makes printing this the required behaviour for a
265    /// failed include, because "file not found" without the list of places that were tried is
266    /// the diagnostic that wastes the most time in this part of the compiler.
267    pub fn tried(
268        &self,
269        name: &str,
270        form: IncludeForm,
271        relative_to: Option<&Path>,
272        from: usize,
273    ) -> Vec<PathBuf> {
274        if is_absolute(Path::new(name)) {
275            return Vec::new();
276        }
277        let mut list = Vec::new();
278        if form == IncludeForm::Quoted {
279            if let Some(dir) = relative_to {
280                list.push(dir.to_path_buf());
281            }
282        }
283        list.extend(self.dirs.iter().skip(from).map(|d| d.path.clone()));
284        list
285    }
286}
287
288/// A path as a string, lossily, because a diagnostic has to say something.
289fn display(path: &Path) -> String {
290    path.to_string_lossy().into_owned()
291}
292
293/// Whether an include name names a file outright rather than one to be searched for.
294///
295/// `Path::is_absolute` is false for `/usr/include/stdio.h` on Windows, because it has no
296/// drive letter. A C file that says `#include "/usr/include/stdio.h"` means a path from the
297/// root whatever host is compiling it, so a leading separator counts here as well.
298fn is_absolute(path: &Path) -> bool {
299    path.is_absolute() || path.has_root()
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    fn fs_with(files: &[&str]) -> MemoryFileSystem {
307        let mut fs = MemoryFileSystem::new();
308        for f in files {
309            fs.insert(*f, format!("/* {f} */\n").into_bytes());
310        }
311        fs
312    }
313
314    fn text(found: &Found) -> String {
315        String::from_utf8_lossy(found.bytes.as_slice()).into_owned()
316    }
317
318    /// A path with forward slashes, because `Path::join` uses a backslash on Windows and
319    /// these tests are about the search order rather than about separators.
320    fn norm(path: &str) -> String {
321        path.replace('\\', "/")
322    }
323
324    #[test]
325    fn a_missing_file_is_not_found_rather_than_an_error() {
326        let fs = MemoryFileSystem::new();
327        let kind = fs.read(Path::new("/nope.h")).err().map(|e| e.kind());
328        assert_eq!(kind, Some(io::ErrorKind::NotFound));
329        assert!(fs.is_empty());
330    }
331
332    #[test]
333    fn quote_directories_are_invisible_to_an_angled_include() {
334        let fs = fs_with(&["/q/a.h", "/i/a.h"]);
335        let mut search = SearchPath::new();
336        search.push_quote("/q");
337        search.push_bracket("/i");
338
339        let quoted = search.resolve(&fs, "a.h", IncludeForm::Quoted, None, 0).unwrap();
340        assert_eq!(norm(&quoted.name), "/q/a.h");
341        let angled = search
342            .resolve(&fs, "a.h", IncludeForm::Angled, None, search.start(IncludeForm::Angled))
343            .unwrap();
344        assert_eq!(norm(&angled.name), "/i/a.h");
345    }
346
347    #[test]
348    fn the_including_files_own_directory_comes_first_for_a_quoted_include() {
349        let fs = fs_with(&["/src/a.h", "/i/a.h"]);
350        let mut search = SearchPath::new();
351        search.push_bracket("/i");
352        let here = Path::new("/src");
353
354        let quoted = search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(here), 0).unwrap();
355        assert_eq!(norm(&quoted.name), "/src/a.h");
356        // An angled include does not look there, even though it was passed.
357        let angled = search.resolve(&fs, "a.h", IncludeForm::Angled, Some(here), 0).unwrap();
358        assert_eq!(norm(&angled.name), "/i/a.h");
359    }
360
361    #[test]
362    fn the_order_is_iquote_then_i_then_isystem_then_idirafter() {
363        let fs = fs_with(&["/after/a.h", "/sys/a.h", "/i/a.h", "/q/a.h"]);
364        let mut search = SearchPath::new();
365        // Pushed in an order that is not the search order, because a driver reads the command
366        // line left to right and the groups interleave.
367        search.push_after("/after");
368        search.push_system("/sys");
369        search.push_bracket("/i");
370        search.push_quote("/q");
371        let order: Vec<_> = search.dirs().iter().map(|d| norm(&d.path.to_string_lossy())).collect();
372        assert_eq!(order, ["/q", "/i", "/sys", "/after"]);
373
374        let found = search.resolve(&fs, "a.h", IncludeForm::Quoted, None, 0).unwrap();
375        assert_eq!(norm(&found.name), "/q/a.h");
376        assert_eq!(found.next, 1);
377    }
378
379    #[test]
380    fn a_system_directory_marks_what_it_holds_as_a_system_header() {
381        let fs = fs_with(&["/i/a.h", "/sys/b.h", "/after/c.h"]);
382        let mut search = SearchPath::new();
383        search.push_bracket("/i");
384        search.push_system("/sys");
385        search.push_after("/after");
386        let get = |n| search.resolve(&fs, n, IncludeForm::Angled, None, 0).unwrap();
387        assert!(!get("a.h").is_system);
388        assert!(get("b.h").is_system);
389        assert!(get("c.h").is_system);
390    }
391
392    #[test]
393    fn include_next_continues_past_the_directory_the_current_file_came_from() {
394        let fs = fs_with(&["/a/limits.h", "/b/limits.h", "/c/limits.h"]);
395        let mut search = SearchPath::new();
396        search.push_bracket("/a");
397        search.push_bracket("/b");
398        search.push_bracket("/c");
399
400        let first = search.resolve(&fs, "limits.h", IncludeForm::Angled, None, 0).unwrap();
401        assert_eq!(norm(&first.name), "/a/limits.h");
402        let second =
403            search.resolve(&fs, "limits.h", IncludeForm::Angled, None, first.next).unwrap();
404        assert_eq!(norm(&second.name), "/b/limits.h");
405        let third =
406            search.resolve(&fs, "limits.h", IncludeForm::Angled, None, second.next).unwrap();
407        assert_eq!(norm(&third.name), "/c/limits.h");
408        assert!(search.resolve(&fs, "limits.h", IncludeForm::Angled, None, third.next).is_none());
409    }
410
411    #[test]
412    fn a_name_with_a_directory_in_it_is_joined_onto_each_entry() {
413        let fs = fs_with(&["/i/sys/types.h"]);
414        let mut search = SearchPath::new();
415        search.push_bracket("/i");
416        let found = search.resolve(&fs, "sys/types.h", IncludeForm::Angled, None, 0).unwrap();
417        assert_eq!(norm(&found.name), "/i/sys/types.h");
418        assert_eq!(text(&found), "/* /i/sys/types.h */\n");
419    }
420
421    #[test]
422    fn an_absolute_name_ignores_the_search_path() {
423        let fs = fs_with(&["/gen/config.h", "/i/gen/config.h"]);
424        let mut search = SearchPath::new();
425        search.push_bracket("/i");
426        let found = search.resolve(&fs, "/gen/config.h", IncludeForm::Angled, None, 0).unwrap();
427        assert_eq!(norm(&found.name), "/gen/config.h");
428        assert!(search.tried("/gen/config.h", IncludeForm::Angled, None, 0).is_empty());
429    }
430
431    #[test]
432    fn the_list_of_places_tried_is_the_list_that_was_searched() {
433        let fs = MemoryFileSystem::new();
434        let mut search = SearchPath::new();
435        search.push_quote("/q");
436        search.push_bracket("/i");
437        search.push_system("/sys");
438        let here = Path::new("/src");
439
440        assert!(search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(here), 0).is_none());
441        let tried = search.tried("a.h", IncludeForm::Quoted, Some(here), 0);
442        let tried: Vec<_> = tried.iter().map(|p| norm(&p.to_string_lossy())).collect();
443        assert_eq!(tried, ["/src", "/q", "/i", "/sys"]);
444
445        let start = search.start(IncludeForm::Angled);
446        let tried = search.tried("a.h", IncludeForm::Angled, Some(here), start);
447        let tried: Vec<_> = tried.iter().map(|p| norm(&p.to_string_lossy())).collect();
448        assert_eq!(tried, ["/i", "/sys"]);
449    }
450
451    #[test]
452    fn a_header_found_next_to_its_includer_does_not_skip_the_whole_path_afterwards() {
453        // `at` for a file found beside its includer has to leave `at + 1` at the top of the
454        // path, because the directory it was found in is not on the path at all.
455        let fs = fs_with(&["/src/a.h", "/i/b.h"]);
456        let mut search = SearchPath::new();
457        search.push_bracket("/i");
458        let found =
459            search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(Path::new("/src")), 0).unwrap();
460        assert_eq!(found.next, 0);
461        let next = search.resolve(&fs, "b.h", IncludeForm::Angled, None, found.next).unwrap();
462        assert_eq!(norm(&next.name), "/i/b.h");
463    }
464}