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}