Skip to main content

rucc_target/
lib.rs

1//! Target descriptions: triples, and the facts about a target that the rest of the
2//! compiler reads rather than hard-codes.
3//!
4//! Design: `spec/12-abi-and-runtime.md`. Layer rank 1, see `spec/18-package-layout.md`.
5//!
6//! The rule from `spec/18-package-layout.md` section 18.2 is that there is no
7//! target-specific code outside this crate and the per-target rule sets. Everything a pass
8//! needs to know about a target is a field it can read here. That rule is what makes the
9//! claim in `spec/10-backend.md` testable, namely that a new target is a rule set and a few
10//! data files, and `M10` brings up a fourth target specifically to put a number on it.
11//!
12//! # Status
13//!
14//! Triple parsing and the basic data model are real, which is what `rucc --print-config`
15//! reports. Register files, machine models and the full ABI descriptions land in `M3`
16//! and `M6`.
17//!
18//! This crate is tier 3 in `spec/18-package-layout.md` section 18.5: its Rust API is
19//! explicitly unstable and will change without a major version bump.
20
21#![doc(html_root_url = "https://docs.rs/rucc-target/0.2.1")]
22
23use std::fmt;
24use std::str::FromStr;
25
26/// A target architecture.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
28// Deliberately not `#[non_exhaustive]`. Adding a variant here has to break every
29// match that needs to change, in this workspace and in anyone else's code. That is
30// the property `spec/10-backend.md` section 10.8 is claiming when it says adding a
31// target is a data change: the compiler tells you every place the data is read.
32pub enum Arch {
33    /// x86-64, the first target and the one `M3` brings up.
34    X86_64,
35    /// AArch64, the second target, `M6`.
36    Aarch64,
37    /// 64-bit RISC-V. `spec/10-backend.md` calls this the middle-end canary, because it has
38    /// no condition codes and no complex addressing modes, so anything the middle end got
39    /// away with on x86-64 shows up here.
40    Riscv64,
41}
42
43impl Arch {
44    /// Pointer width in bits.
45    pub const fn pointer_width(self) -> u32 {
46        match self {
47            Arch::X86_64 | Arch::Aarch64 | Arch::Riscv64 => 64,
48        }
49    }
50
51    /// Whether the target is little-endian.
52    pub const fn is_little_endian(self) -> bool {
53        match self {
54            Arch::X86_64 | Arch::Aarch64 | Arch::Riscv64 => true,
55        }
56    }
57
58    /// The name as it appears in a triple.
59    pub const fn as_str(self) -> &'static str {
60        match self {
61            Arch::X86_64 => "x86_64",
62            Arch::Aarch64 => "aarch64",
63            Arch::Riscv64 => "riscv64",
64        }
65    }
66}
67
68/// The operating system a target runs on.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
70// Deliberately not `#[non_exhaustive]`. Adding a variant here has to break every
71// match that needs to change, in this workspace and in anyone else's code. That is
72// the property `spec/10-backend.md` section 10.8 is claiming when it says adding a
73// target is a data change: the compiler tells you every place the data is read.
74pub enum Os {
75    /// Linux, hosted or freestanding.
76    Linux,
77    /// Apple platforms. `spec/12-abi-and-runtime.md` section 12.3 lists the four places
78    /// Apple diverges from AAPCS64, and every one of them is a real bug if missed.
79    Darwin,
80    /// Windows.
81    Windows,
82    /// No operating system, which is what `-ffreestanding` kernel work looks like.
83    None,
84}
85
86impl Os {
87    /// The name as it appears in a triple.
88    pub const fn as_str(self) -> &'static str {
89        match self {
90            Os::Linux => "linux",
91            Os::Darwin => "darwin",
92            Os::Windows => "windows",
93            Os::None => "none",
94        }
95    }
96
97    /// The object file format this operating system uses.
98    pub const fn object_format(self) -> ObjectFormat {
99        match self {
100            Os::Linux | Os::None => ObjectFormat::Elf,
101            Os::Darwin => ObjectFormat::MachO,
102            Os::Windows => ObjectFormat::Coff,
103        }
104    }
105}
106
107/// The C runtime and ABI variant.
108#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
109// Deliberately not `#[non_exhaustive]`. Adding a variant here has to break every
110// match that needs to change, in this workspace and in anyone else's code. That is
111// the property `spec/10-backend.md` section 10.8 is claiming when it says adding a
112// target is a data change: the compiler tells you every place the data is read.
113pub enum Env {
114    /// The default for the operating system.
115    None,
116    /// glibc.
117    Gnu,
118    /// musl.
119    Musl,
120    /// The MSVC ABI.
121    Msvc,
122}
123
124impl Env {
125    /// The name as it appears in a triple, if it appears at all.
126    pub const fn as_str(self) -> &'static str {
127        match self {
128            Env::None => "none",
129            Env::Gnu => "gnu",
130            Env::Musl => "musl",
131            Env::Msvc => "msvc",
132        }
133    }
134}
135
136/// The object file format to emit.
137#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
138// Deliberately not `#[non_exhaustive]`. Adding a variant here has to break every
139// match that needs to change, in this workspace and in anyone else's code. That is
140// the property `spec/10-backend.md` section 10.8 is claiming when it says adding a
141// target is a data change: the compiler tells you every place the data is read.
142pub enum ObjectFormat {
143    /// ELF.
144    Elf,
145    /// Mach-O.
146    MachO,
147    /// COFF.
148    Coff,
149}
150
151impl ObjectFormat {
152    /// The name used in diagnostics and in `--print-config`.
153    pub const fn as_str(self) -> &'static str {
154        match self {
155            ObjectFormat::Elf => "elf",
156            ObjectFormat::MachO => "macho",
157            ObjectFormat::Coff => "coff",
158        }
159    }
160}
161
162/// A target triple.
163///
164/// We accept the LLVM-style `arch-vendor-os-env` form because that is what build systems
165/// pass, and we normalise it to the three fields we actually branch on. The vendor field is
166/// parsed and discarded: no decision in the compiler depends on it, and keeping it would
167/// invite one.
168#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
169pub struct Triple {
170    /// The architecture.
171    pub arch: Arch,
172    /// The operating system.
173    pub os: Os,
174    /// The runtime and ABI variant.
175    pub env: Env,
176}
177
178impl Triple {
179    /// A triple from its three parts.
180    pub const fn new(arch: Arch, os: Os, env: Env) -> Self {
181        Self { arch, os, env }
182    }
183
184    /// The triple of the machine this compiler is running on.
185    ///
186    /// Used as the default target, which is what makes `rucc hello.c` work with no flags.
187    /// Unknown host combinations are not an error here: they are reported by the driver,
188    /// where there is somewhere to report them to.
189    pub fn host() -> Option<Self> {
190        let arch = match std::env::consts::ARCH {
191            "x86_64" => Arch::X86_64,
192            "aarch64" => Arch::Aarch64,
193            "riscv64" => Arch::Riscv64,
194            _ => return None,
195        };
196        // Which libc this is matters, and `std::env::consts` does not say. A compiler built on
197        // Alpine and defaulting to `x86_64-unknown-linux-gnu` describes a machine it is not
198        // running on: musl and glibc disagree about `int_fast16_t` among other things, and a
199        // header that is written out of the predefined type names picks the disagreement up.
200        // The libc rucc itself was linked against is the best evidence available about the one
201        // the code it compiles will be linked against, and it is right on every machine where
202        // rucc was built for the machine it runs on.
203        let linux = if cfg!(target_env = "musl") { Env::Musl } else { Env::Gnu };
204        let (os, env) = match std::env::consts::OS {
205            "linux" => (Os::Linux, linux),
206            "macos" => (Os::Darwin, Env::None),
207            "windows" => (Os::Windows, Env::Msvc),
208            _ => return None,
209        };
210        Some(Self::new(arch, os, env))
211    }
212}
213
214impl fmt::Display for Triple {
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        // Always four fields, always the same spelling, because this string ends up in
217        // `--print-config` output that people diff.
218        write!(f, "{}-unknown-{}-{}", self.arch.as_str(), self.os.as_str(), self.env.as_str())
219    }
220}
221
222/// Why a triple failed to parse.
223#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct ParseTripleError {
225    /// The triple as given.
226    pub input: String,
227    /// What specifically was not recognised.
228    pub reason: &'static str,
229}
230
231impl fmt::Display for ParseTripleError {
232    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233        write!(f, "unsupported target triple `{}`: {}", self.input, self.reason)
234    }
235}
236
237impl std::error::Error for ParseTripleError {}
238
239impl FromStr for Triple {
240    type Err = ParseTripleError;
241
242    fn from_str(s: &str) -> Result<Self, Self::Err> {
243        let err = |reason| ParseTripleError { input: s.to_owned(), reason };
244        let mut parts = s.split('-');
245
246        let arch = match parts.next() {
247            Some("x86_64" | "amd64") => Arch::X86_64,
248            Some("aarch64" | "arm64") => Arch::Aarch64,
249            Some("riscv64") => Arch::Riscv64,
250            _ => return Err(err("unknown architecture")),
251        };
252
253        // The vendor field is optional in practice. `x86_64-linux-gnu` and
254        // `x86_64-unknown-linux-gnu` both occur in the wild and mean the same thing, so the
255        // remaining fields are matched by content rather than by position.
256        let rest: Vec<&str> = parts.collect();
257        let mut os = None;
258        let mut env = None;
259        for part in &rest {
260            match *part {
261                "linux" => os = Some(Os::Linux),
262                "darwin" | "macos" | "macosx" | "ios" => os = Some(Os::Darwin),
263                "windows" | "win32" => os = Some(Os::Windows),
264                // `none` is the one token that means different things in the two positions.
265                // In `x86_64-unknown-none-elf` it is the operating system; in
266                // `aarch64-apple-darwin-none` it is the environment. Which one it is depends
267                // on whether an operating system has already been seen, and that rule is what
268                // makes `Display` round-trip through `FromStr`.
269                "none" if os.is_none() => os = Some(Os::None),
270                "none" => env = Some(Env::None),
271                "elf" => os = os.or(Some(Os::None)),
272                "gnu" | "gnueabi" | "gnueabihf" => env = Some(Env::Gnu),
273                "musl" | "musleabi" | "musleabihf" => env = Some(Env::Musl),
274                "msvc" => env = Some(Env::Msvc),
275                _ => {}
276            }
277        }
278
279        let os = os.ok_or_else(|| err("unknown operating system"))?;
280        let env = env.unwrap_or(match os {
281            Os::Linux => Env::Gnu,
282            Os::Windows => Env::Msvc,
283            Os::Darwin | Os::None => Env::None,
284        });
285        Ok(Self::new(arch, os, env))
286    }
287}
288
289/// The facts about a target that the compiler reads instead of hard-coding.
290///
291/// This is the whole of what a pass is allowed to know about where its output will run.
292/// It grows, and every field added here is one fewer `#[cfg]` somewhere it should not be.
293#[derive(Debug, Clone, PartialEq, Eq)]
294#[non_exhaustive]
295pub struct TargetInfo {
296    /// The triple this describes.
297    pub triple: Triple,
298    /// Width of a pointer in bits.
299    pub pointer_width: u32,
300    /// Whether bytes are ordered little end first.
301    pub little_endian: bool,
302    /// Whether a bare `char` is signed.
303    ///
304    /// Signed on x86-64 and unsigned on AArch64 Linux, which is the classic source of code
305    /// that works on one and not the other, so it is data rather than an assumption.
306    pub char_is_signed: bool,
307    /// Width of `long` in bits. This is the field that separates the LP64 world from
308    /// Windows LLP64.
309    pub long_width: u32,
310    /// Width of `long double` in bits: 80 bits of x87 stored in 128 on SysV x86-64,
311    /// 64 on Apple platforms, 64 on Windows.
312    pub long_double_width: u32,
313    /// The granule a `_BitInt` wider than 64 bits is laid out in, in bits.
314    ///
315    /// Above 64 bits the psABIs stop treating a `_BitInt` like a standard integer type and
316    /// start treating it like an array of these, so its size is rounded up to a multiple of
317    /// this and its alignment is this. It is 64 on x86-64 and RISC-V and 128 on AArch64, which
318    /// is why `_BitInt(65)` is sixteen bytes aligned to eight on one and sixteen bytes aligned
319    /// to sixteen on the other. Measured with clang 18 on x86-64 Linux and clang on AArch64
320    /// Darwin rather than read off the documents.
321    pub bit_int_granule: u32,
322    /// The object format to emit.
323    pub object_format: ObjectFormat,
324}
325
326impl TargetInfo {
327    /// The description of `triple`.
328    pub fn new(triple: Triple) -> Self {
329        let char_is_signed = match (triple.arch, triple.os) {
330            // The AArch64 and RISC-V psABIs make plain `char` unsigned, and x86-64 SysV
331            // makes it signed. Apple and Windows both override that back to signed on
332            // AArch64, which is the kind of divergence that only ever surfaces as a bug
333            // report from someone whose lexer compares a `char` against a negative value.
334            (Arch::Aarch64 | Arch::Riscv64, Os::Linux | Os::None) => false,
335            _ => true,
336        };
337        let long_width = match triple.os {
338            // Windows is LLP64: `long` stays 32 bits on a 64-bit target.
339            Os::Windows => 32,
340            _ => triple.arch.pointer_width(),
341        };
342        let long_double_width = match triple.os {
343            // Apple defines `long double` as `double`, per spec/12-abi-and-runtime.md
344            // section 12.3, and Windows does the same. On the SysV targets it is a distinct
345            // type: 80 bits of x87 stored in 128 on x86-64, and true quad precision on
346            // AArch64 and RISC-V.
347            Os::Darwin | Os::Windows => 64,
348            Os::Linux | Os::None => 128,
349        };
350        let bit_int_granule = match triple.arch {
351            Arch::Aarch64 => 128,
352            Arch::X86_64 | Arch::Riscv64 => 64,
353        };
354        Self {
355            triple,
356            pointer_width: triple.arch.pointer_width(),
357            little_endian: triple.arch.is_little_endian(),
358            char_is_signed,
359            long_width,
360            long_double_width,
361            bit_int_granule,
362            object_format: triple.os.object_format(),
363        }
364    }
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370
371    #[test]
372    fn parses_a_four_field_triple() {
373        let t: Triple = "x86_64-unknown-linux-gnu".parse().unwrap();
374        assert_eq!(t, Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
375    }
376
377    #[test]
378    fn parses_a_triple_with_no_vendor() {
379        let t: Triple = "aarch64-linux-musl".parse().unwrap();
380        assert_eq!(t, Triple::new(Arch::Aarch64, Os::Linux, Env::Musl));
381    }
382
383    #[test]
384    fn accepts_the_common_aliases() {
385        let a: Triple = "arm64-apple-darwin".parse().unwrap();
386        let b: Triple = "aarch64-apple-darwin".parse().unwrap();
387        assert_eq!(a, b);
388        assert_eq!(a.env, Env::None);
389    }
390
391    #[test]
392    fn fills_in_the_default_environment() {
393        let t: Triple = "x86_64-unknown-linux".parse().unwrap();
394        assert_eq!(t.env, Env::Gnu);
395        let w: Triple = "x86_64-pc-windows".parse().unwrap();
396        assert_eq!(w.env, Env::Msvc);
397    }
398
399    #[test]
400    fn rejects_what_it_does_not_support() {
401        let e = "sparc64-unknown-linux-gnu".parse::<Triple>().unwrap_err();
402        assert_eq!(e.reason, "unknown architecture");
403        let e = "x86_64-unknown-plan9".parse::<Triple>().unwrap_err();
404        assert_eq!(e.reason, "unknown operating system");
405    }
406
407    #[test]
408    fn displays_in_a_normalised_form() {
409        let t: Triple = "amd64-linux-gnu".parse().unwrap();
410        assert_eq!(t.to_string(), "x86_64-unknown-linux-gnu");
411    }
412
413    #[test]
414    fn display_round_trips_through_parse() {
415        for s in [
416            "x86_64-unknown-linux-gnu",
417            "aarch64-unknown-darwin-none",
418            "riscv64-unknown-linux-musl",
419        ] {
420            let t: Triple = s.parse().unwrap();
421            assert_eq!(t.to_string().parse::<Triple>().unwrap(), t);
422        }
423    }
424
425    #[test]
426    fn char_signedness_follows_the_psabi() {
427        let x86 = TargetInfo::new("x86_64-unknown-linux-gnu".parse().unwrap());
428        let arm = TargetInfo::new("aarch64-unknown-linux-gnu".parse().unwrap());
429        let mac = TargetInfo::new("aarch64-apple-darwin".parse().unwrap());
430        assert!(x86.char_is_signed);
431        assert!(!arm.char_is_signed);
432        assert!(mac.char_is_signed, "Apple overrides AAPCS64 back to a signed char");
433    }
434
435    #[test]
436    fn windows_is_llp64() {
437        let win = TargetInfo::new("x86_64-pc-windows-msvc".parse().unwrap());
438        assert_eq!(win.pointer_width, 64);
439        assert_eq!(win.long_width, 32);
440    }
441
442    #[test]
443    fn apple_long_double_is_double() {
444        let mac = TargetInfo::new("aarch64-apple-darwin".parse().unwrap());
445        assert_eq!(mac.long_double_width, 64);
446        let linux = TargetInfo::new("x86_64-unknown-linux-gnu".parse().unwrap());
447        assert_eq!(linux.long_double_width, 128);
448    }
449
450    #[test]
451    fn the_object_format_follows_the_operating_system() {
452        assert_eq!(Os::Linux.object_format(), ObjectFormat::Elf);
453        assert_eq!(Os::Darwin.object_format(), ObjectFormat::MachO);
454        assert_eq!(Os::Windows.object_format(), ObjectFormat::Coff);
455    }
456
457    #[test]
458    fn the_host_triple_is_one_we_support() {
459        // Every host in spec/15-testing.md section 15.7 must be recognised, and CI runs on
460        // all three, so a failure here means a host we claim support for stopped resolving.
461        let host = Triple::host().expect("the host must be a supported target");
462        assert_eq!(host.to_string().parse::<Triple>().unwrap(), host);
463    }
464}