Skip to main content

rucc_session/
runtime.rs

1//! The headers the compiler ships, and the directory they appear to live in.
2//!
3//! Design: `spec/04-driver-and-cli.md` section 4.4.
4//!
5//! A hosted C implementation is two halves. The library ships `<stdio.h>` and everything that
6//! declares a function you link against. The compiler ships the handful of headers whose
7//! contents are not the library's to know: `<stdarg.h>` is the target's calling convention,
8//! `<limits.h>` and `<float.h>` are the target's types, and `<stddef.h>` is the ABI. No
9//! library can write those, which is why every compiler carries its own copies and why a
10//! compiler that carries none cannot preprocess a program as ordinary as SQLite.
11//!
12//! They are in the binary rather than on disk. A compiler that has to find its own
13//! installation directory before it can preprocess a file is a compiler that stops working
14//! when it is copied somewhere else, and a single static binary that works from anywhere is
15//! worth more here than the ability to edit a header without rebuilding.
16//!
17//! Since they are not on disk they need a name, because the search path is a list of
18//! directories and a diagnostic has to be able to say where a header came from. That name is
19//! [`DIR`], and the angle brackets are the point: no directory a user can create is spelled
20//! that way, so nothing on the real file system can shadow these or be shadowed by them.
21
22use std::path::Path;
23
24use rucc_diag::SourceBytes;
25
26/// The directory the shipped headers appear to be in.
27///
28/// Not a path. It is a name that cannot be one, so that `#include <stdarg.h>` resolving to
29/// `<builtin>/stdarg.h` reads as what it is, and so that a real directory can never collide
30/// with it.
31pub const DIR: &str = "<builtin>";
32
33/// Every shipped header, in the order they are listed here, which is sorted by name.
34///
35/// The text is in the binary. `include_str!` rather than a build script because the set is
36/// small and fixed, and because a build script would put the headers behind a step that has
37/// to run before anything can be read.
38const HEADERS: &[(&str, &str)] = &[
39    ("float.h", include_str!("../runtime/include/float.h")),
40    ("iso646.h", include_str!("../runtime/include/iso646.h")),
41    ("limits.h", include_str!("../runtime/include/limits.h")),
42    ("stdalign.h", include_str!("../runtime/include/stdalign.h")),
43    ("stdarg.h", include_str!("../runtime/include/stdarg.h")),
44    ("stdbool.h", include_str!("../runtime/include/stdbool.h")),
45    ("stddef.h", include_str!("../runtime/include/stddef.h")),
46    ("stdint.h", include_str!("../runtime/include/stdint.h")),
47    ("stdnoreturn.h", include_str!("../runtime/include/stdnoreturn.h")),
48];
49
50/// The names of the shipped headers, sorted.
51#[must_use]
52pub fn names() -> Vec<&'static str> {
53    HEADERS.iter().map(|&(name, _)| name).collect()
54}
55
56/// The text of one shipped header, by its name alone.
57#[must_use]
58pub fn header(name: &str) -> Option<&'static str> {
59    HEADERS.iter().find(|&&(have, _)| have == name).map(|&(_, text)| text)
60}
61
62/// Reads a path that an include search produced, when it names a shipped header.
63///
64/// The path is [`DIR`] joined with the header's name, which on Windows means a backslash
65/// between them, so the two halves are compared rather than the string.
66#[must_use]
67pub fn read(path: &Path) -> Option<SourceBytes> {
68    if path.parent() != Some(Path::new(DIR)) {
69        return None;
70    }
71    let name = path.file_name()?.to_str()?;
72    header(name).map(SourceBytes::new)
73}
74
75#[cfg(test)]
76mod tests {
77    use std::path::PathBuf;
78
79    use super::*;
80
81    #[test]
82    fn the_shipped_headers_are_read_by_the_name_the_search_path_builds() {
83        let path = PathBuf::from(DIR).join("stdarg.h");
84        let bytes = read(&path).expect("stdarg.h is shipped");
85        let text = String::from_utf8(bytes.as_ref().to_vec()).expect("utf-8");
86        assert!(text.contains("__builtin_va_list"));
87    }
88
89    #[test]
90    fn nothing_outside_the_builtin_directory_is_answered() {
91        assert!(read(Path::new("/usr/include/stdarg.h")).is_none());
92        assert!(read(Path::new("stdarg.h")).is_none());
93        assert!(read(&PathBuf::from(DIR).join("stdio.h")).is_none());
94        assert!(read(&PathBuf::from(DIR).join("sys").join("stdarg.h")).is_none());
95    }
96
97    #[test]
98    fn the_list_is_sorted_so_that_a_new_header_has_one_place_to_go() {
99        let mut sorted = names();
100        sorted.sort_unstable();
101        assert_eq!(names(), sorted);
102    }
103
104    /// Every header has to be idempotent and has to name itself in its own guard, because a
105    /// program includes `<stddef.h>` forty times and a guard copied from a neighbour is the
106    /// way one of them silently stops working.
107    #[test]
108    fn every_header_guards_itself_under_its_own_name() {
109        for &(name, text) in HEADERS {
110            let guard = format!("__RUCC_{}", name.trim_end_matches(".h").to_uppercase());
111            assert!(text.contains(&guard), "{name} does not mention {guard}");
112        }
113    }
114}