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 ("stdatomic.h", include_str!("../runtime/include/stdatomic.h")),
45 ("stdbool.h", include_str!("../runtime/include/stdbool.h")),
46 ("stddef.h", include_str!("../runtime/include/stddef.h")),
47 ("stdint.h", include_str!("../runtime/include/stdint.h")),
48 ("stdnoreturn.h", include_str!("../runtime/include/stdnoreturn.h")),
49];
50
51/// The names of the shipped headers, sorted.
52#[must_use]
53pub fn names() -> Vec<&'static str> {
54 HEADERS.iter().map(|&(name, _)| name).collect()
55}
56
57/// The text of one shipped header, by its name alone.
58#[must_use]
59pub fn header(name: &str) -> Option<&'static str> {
60 HEADERS.iter().find(|&&(have, _)| have == name).map(|&(_, text)| text)
61}
62
63/// Reads a path that an include search produced, when it names a shipped header.
64///
65/// The path is [`DIR`] joined with the header's name, which on Windows means a backslash
66/// between them, so the two halves are compared rather than the string.
67#[must_use]
68pub fn read(path: &Path) -> Option<SourceBytes> {
69 if path.parent() != Some(Path::new(DIR)) {
70 return None;
71 }
72 let name = path.file_name()?.to_str()?;
73 header(name).map(SourceBytes::new)
74}
75
76#[cfg(test)]
77mod tests {
78 use std::path::PathBuf;
79
80 use super::*;
81
82 #[test]
83 fn the_shipped_headers_are_read_by_the_name_the_search_path_builds() {
84 let path = PathBuf::from(DIR).join("stdarg.h");
85 let bytes = read(&path).expect("stdarg.h is shipped");
86 let text = String::from_utf8(bytes.as_ref().to_vec()).expect("utf-8");
87 assert!(text.contains("__builtin_va_list"));
88 }
89
90 #[test]
91 fn nothing_outside_the_builtin_directory_is_answered() {
92 assert!(read(Path::new("/usr/include/stdarg.h")).is_none());
93 assert!(read(Path::new("stdarg.h")).is_none());
94 assert!(read(&PathBuf::from(DIR).join("stdio.h")).is_none());
95 assert!(read(&PathBuf::from(DIR).join("sys").join("stdarg.h")).is_none());
96 }
97
98 #[test]
99 fn the_list_is_sorted_so_that_a_new_header_has_one_place_to_go() {
100 let mut sorted = names();
101 sorted.sort_unstable();
102 assert_eq!(names(), sorted);
103 }
104
105 /// Every header has to be idempotent and has to name itself in its own guard, because a
106 /// program includes `<stddef.h>` forty times and a guard copied from a neighbour is the
107 /// way one of them silently stops working.
108 #[test]
109 fn every_header_guards_itself_under_its_own_name() {
110 for &(name, text) in HEADERS {
111 let guard = format!("__RUCC_{}", name.trim_end_matches(".h").to_uppercase());
112 assert!(text.contains(&guard), "{name} does not mention {guard}");
113 }
114 }
115}