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