Skip to main content

gruel_target/
lib.rs

1//! Target architecture and OS definitions for the Gruel compiler.
2//!
3//! Backed by [`target_lexicon::Triple`], so any triple LLVM understands can be
4//! parsed and used. The compiler-internal `Arch` and `Os` enums are derived
5//! from the lexicon's fields rather than hardcoded.
6//!
7//! See ADR-0077 for the design.
8
9use std::fmt;
10use std::str::FromStr;
11
12use target_lexicon::{Architecture, BinaryFormat, OperatingSystem, Triple};
13
14/// A compilation target, identified by an LLVM-style triple.
15///
16/// `Target` wraps [`target_lexicon::Triple`] so the parser, validator, and
17/// host-detection logic come "for free" from the upstream crate. Anything
18/// LLVM understands is accepted; only the targets in [`Target::all()`] are
19/// "blessed" (i.e. tested and supported).
20#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21pub struct Target {
22    triple: Triple,
23}
24
25impl Target {
26    /// The host machine's target, evaluated at compile time.
27    pub fn host() -> Self {
28        Self {
29            triple: Triple::host(),
30        }
31    }
32
33    /// Construct a target from a [`target_lexicon::Triple`].
34    pub fn from_triple(triple: Triple) -> Self {
35        Self { triple }
36    }
37
38    /// The underlying [`target_lexicon::Triple`].
39    pub fn triple(&self) -> &Triple {
40        &self.triple
41    }
42
43    /// The triple as a string suitable for passing to LLVM (e.g.
44    /// `"x86_64-unknown-linux-gnu"`).
45    pub fn triple_string(&self) -> String {
46        self.triple.to_string()
47    }
48
49    /// The CPU architecture component of this target.
50    pub fn arch(&self) -> Arch {
51        Arch::from_lexicon(self.triple.architecture)
52    }
53
54    /// The operating system component of this target.
55    pub fn os(&self) -> Os {
56        Os::from_lexicon(self.triple.operating_system)
57    }
58
59    /// Whether this target uses ELF object format.
60    pub fn is_elf(&self) -> bool {
61        self.triple.binary_format == BinaryFormat::Elf
62    }
63
64    /// Whether this target uses Mach-O object format.
65    pub fn is_macho(&self) -> bool {
66        self.triple.binary_format == BinaryFormat::Macho
67    }
68
69    /// The curated list of "blessed" targets — those Gruel explicitly tests
70    /// and supports. Other LLVM-known triples are accepted (`from_str`
71    /// succeeds) but unblessed.
72    pub fn all() -> Vec<Target> {
73        BLESSED_TRIPLES
74            .iter()
75            .map(|s| Target::from_str(s).expect("blessed triple must parse"))
76            .collect()
77    }
78
79    /// Whether the triple is in the blessed list.
80    pub fn is_blessed(&self) -> bool {
81        Self::all().iter().any(|t| t == self)
82    }
83
84    /// Comma-separated string of all blessed target names for help text.
85    pub fn all_names() -> String {
86        Self::all()
87            .iter()
88            .map(|t| t.to_string())
89            .collect::<Vec<_>>()
90            .join(", ")
91    }
92}
93
94/// Blessed target triples — fully tested in CI.
95const BLESSED_TRIPLES: &[&str] = &[
96    "x86_64-unknown-linux-gnu",
97    "aarch64-unknown-linux-gnu",
98    "aarch64-apple-darwin",
99];
100
101impl FromStr for Target {
102    type Err = TargetParseError;
103
104    /// Parse a target from any LLVM-understood triple. Accepts a few
105    /// short-form aliases (`x86_64-linux`, `aarch64-linux`,
106    /// `aarch64-macos`, `arm64-linux`, `arm64-macos`) used historically by
107    /// the CLI.
108    fn from_str(s: &str) -> Result<Self, Self::Err> {
109        let normalized = match s {
110            "x86_64-linux" | "x86-64-linux" => "x86_64-unknown-linux-gnu",
111            "aarch64-linux" | "arm64-linux" => "aarch64-unknown-linux-gnu",
112            "aarch64-macos" | "arm64-macos" => "aarch64-apple-darwin",
113            other => other,
114        };
115        let triple = Triple::from_str(normalized).map_err(|e| TargetParseError {
116            input: s.to_string(),
117            message: e.to_string(),
118        })?;
119        Ok(Target { triple })
120    }
121}
122
123/// Error returned when a target triple fails to parse.
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct TargetParseError {
126    pub input: String,
127    pub message: String,
128}
129
130impl fmt::Display for TargetParseError {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        write!(f, "invalid target '{}': {}", self.input, self.message)
133    }
134}
135
136impl std::error::Error for TargetParseError {}
137
138impl fmt::Display for Target {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        write!(f, "{}", self.triple)
141    }
142}
143
144impl Default for Target {
145    fn default() -> Self {
146        Self::host()
147    }
148}
149
150/// The CPU architecture of a target.
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
152pub enum Arch {
153    X86,
154    X86_64,
155    Arm,
156    Aarch64,
157    Riscv32,
158    Riscv64,
159    Wasm32,
160    Wasm64,
161    /// Any architecture we don't model individually (`target-lexicon` may know
162    /// it, but Gruel hasn't classified it).
163    Unknown,
164}
165
166impl Arch {
167    fn from_lexicon(a: Architecture) -> Self {
168        match a {
169            Architecture::X86_32(_) => Arch::X86,
170            Architecture::X86_64 | Architecture::X86_64h => Arch::X86_64,
171            Architecture::Arm(_) => Arch::Arm,
172            Architecture::Aarch64(_) => Arch::Aarch64,
173            Architecture::Riscv32(_) => Arch::Riscv32,
174            Architecture::Riscv64(_) => Arch::Riscv64,
175            Architecture::Wasm32 => Arch::Wasm32,
176            Architecture::Wasm64 => Arch::Wasm64,
177            _ => Arch::Unknown,
178        }
179    }
180}
181
182impl fmt::Display for Arch {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        let s = match self {
185            Arch::X86 => "x86",
186            Arch::X86_64 => "x86-64",
187            Arch::Arm => "arm",
188            Arch::Aarch64 => "aarch64",
189            Arch::Riscv32 => "riscv32",
190            Arch::Riscv64 => "riscv64",
191            Arch::Wasm32 => "wasm32",
192            Arch::Wasm64 => "wasm64",
193            Arch::Unknown => "unknown",
194        };
195        f.write_str(s)
196    }
197}
198
199/// The operating system of a target.
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
201pub enum Os {
202    Linux,
203    Macos,
204    Windows,
205    /// No OS — bare metal / freestanding.
206    Freestanding,
207    /// WebAssembly System Interface.
208    Wasi,
209    /// Any OS we don't model individually.
210    Unknown,
211}
212
213impl Os {
214    fn from_lexicon(o: OperatingSystem) -> Self {
215        match o {
216            OperatingSystem::Linux => Os::Linux,
217            OperatingSystem::Darwin(_) | OperatingSystem::MacOSX(_) | OperatingSystem::IOS(_) => {
218                // We treat all Apple OSes as Macos for our intrinsic; iOS/etc.
219                // are unusual targets and currently uninteresting.
220                Os::Macos
221            }
222            OperatingSystem::Windows => Os::Windows,
223            OperatingSystem::Wasi | OperatingSystem::WasiP1 | OperatingSystem::WasiP2 => Os::Wasi,
224            OperatingSystem::None_ | OperatingSystem::Unknown => Os::Freestanding,
225            _ => Os::Unknown,
226        }
227    }
228}
229
230impl fmt::Display for Os {
231    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232        let s = match self {
233            Os::Linux => "linux",
234            Os::Macos => "macos",
235            Os::Windows => "windows",
236            Os::Freestanding => "freestanding",
237            Os::Wasi => "wasi",
238            Os::Unknown => "unknown",
239        };
240        f.write_str(s)
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    fn t(s: &str) -> Target {
249        s.parse().unwrap()
250    }
251
252    #[test]
253    fn parses_canonical_triples() {
254        assert_eq!(t("x86_64-unknown-linux-gnu").arch(), Arch::X86_64);
255        assert_eq!(t("x86_64-unknown-linux-gnu").os(), Os::Linux);
256        assert_eq!(t("aarch64-unknown-linux-gnu").arch(), Arch::Aarch64);
257        assert_eq!(t("aarch64-unknown-linux-gnu").os(), Os::Linux);
258        assert_eq!(t("aarch64-apple-darwin").arch(), Arch::Aarch64);
259        assert_eq!(t("aarch64-apple-darwin").os(), Os::Macos);
260    }
261
262    #[test]
263    fn parses_short_aliases() {
264        assert_eq!(t("x86_64-linux"), t("x86_64-unknown-linux-gnu"));
265        assert_eq!(t("x86-64-linux"), t("x86_64-unknown-linux-gnu"));
266        assert_eq!(t("aarch64-linux"), t("aarch64-unknown-linux-gnu"));
267        assert_eq!(t("arm64-linux"), t("aarch64-unknown-linux-gnu"));
268        assert_eq!(t("aarch64-macos"), t("aarch64-apple-darwin"));
269        assert_eq!(t("arm64-macos"), t("aarch64-apple-darwin"));
270    }
271
272    #[test]
273    fn parses_extended_triples() {
274        // These are not "blessed" but must parse.
275        assert_eq!(t("riscv64gc-unknown-linux-gnu").arch(), Arch::Riscv64);
276        assert_eq!(t("riscv32imc-unknown-none-elf").arch(), Arch::Riscv32);
277        assert_eq!(t("wasm32-unknown-unknown").arch(), Arch::Wasm32);
278        assert_eq!(t("x86_64-pc-windows-msvc").os(), Os::Windows);
279        assert_eq!(t("aarch64-unknown-none").os(), Os::Freestanding);
280        assert_eq!(t("wasm32-wasi").os(), Os::Wasi);
281    }
282
283    #[test]
284    fn rejects_garbage() {
285        assert!("not-a-triple-at-all".parse::<Target>().is_err());
286    }
287
288    #[test]
289    fn binary_format() {
290        assert!(t("x86_64-unknown-linux-gnu").is_elf());
291        assert!(t("aarch64-unknown-linux-gnu").is_elf());
292        assert!(!t("aarch64-unknown-linux-gnu").is_macho());
293        assert!(t("aarch64-apple-darwin").is_macho());
294        assert!(!t("aarch64-apple-darwin").is_elf());
295    }
296
297    #[test]
298    fn default_is_host() {
299        assert_eq!(Target::default(), Target::host());
300    }
301
302    #[test]
303    fn blessed_targets_round_trip() {
304        for target in Target::all() {
305            let s = target.to_string();
306            let parsed: Target = s.parse().expect("Display output should re-parse");
307            assert_eq!(target, parsed);
308            assert!(target.is_blessed());
309        }
310    }
311
312    #[test]
313    fn unblessed_target_is_not_blessed() {
314        let t = "wasm32-wasi".parse::<Target>().unwrap();
315        assert!(!t.is_blessed());
316    }
317
318    #[test]
319    fn arch_display_matches_legacy() {
320        assert_eq!(Arch::X86_64.to_string(), "x86-64");
321        assert_eq!(Arch::Aarch64.to_string(), "aarch64");
322    }
323
324    #[test]
325    fn os_display_matches_legacy() {
326        assert_eq!(Os::Linux.to_string(), "linux");
327        assert_eq!(Os::Macos.to_string(), "macos");
328    }
329}