rucc_tuple/tuple.rs
1//! The tuple itself: ten fields, one canonical spelling, and a parser that refuses to guess.
2
3use core::fmt;
4use core::str::FromStr;
5
6use crate::{Abi, Arch, DataModel, Endian, Env, Error, ObjectFormat, Os, SubArch, Version};
7
8/// Everything about a target that changes the bytes the compiler emits.
9///
10/// The membership rule, from `spec/cross-compile/03-target-model.md` section 3.2: a fact belongs here if it
11/// changes how a function is called or how a struct is laid out. Everything else, the CPU
12/// model, the optimization level, the instruction set extensions, is a flag and lives outside.
13/// The rule is what makes the tuple usable as a cache key, and the cache is what makes the
14/// distribution in `spec/cross-compile/13-distribution.md` fit in the size budget, so this is load bearing
15/// rather than tidy.
16///
17/// Construct with [`TargetTuple::new`] or by parsing. The fields are private because six of the
18/// ten are derived from the other four, and a struct literal would let a caller build a
19/// combination that does not exist, such as Windows with an ELF object format.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub struct TargetTuple {
22 arch: Arch,
23 sub_arch: SubArch,
24 endian: Endian,
25 data_model: DataModel,
26 os: Os,
27 os_version: Option<Version>,
28 env: Env,
29 env_version: Option<Version>,
30 abi: Abi,
31 object_format: ObjectFormat,
32}
33
34/// The parts a caller supplies, with the rest derived.
35///
36/// A builder rather than ten arguments, because eight of the ten calls in this workspace supply
37/// three of them and a function with seven defaulted parameters is a function nobody calls
38/// correctly.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct TupleBuilder {
41 arch: Arch,
42 sub_arch: SubArch,
43 endian: Option<Endian>,
44 data_model: Option<DataModel>,
45 os: Os,
46 os_version: Option<Version>,
47 env: Option<Env>,
48 env_version: Option<Version>,
49 abi: Abi,
50}
51
52impl TupleBuilder {
53 /// Start from the two fields that have no default.
54 pub const fn new(arch: Arch, os: Os) -> Self {
55 TupleBuilder {
56 arch,
57 sub_arch: SubArch::None,
58 endian: None,
59 data_model: None,
60 os,
61 os_version: None,
62 env: None,
63 env_version: None,
64 abi: Abi::Default,
65 }
66 }
67
68 /// Set the baseline within the architecture family.
69 pub const fn sub_arch(mut self, sub_arch: SubArch) -> Self {
70 self.sub_arch = sub_arch;
71 self
72 }
73
74 /// Set the byte order, overriding the architecture's default.
75 pub const fn endian(mut self, endian: Endian) -> Self {
76 self.endian = Some(endian);
77 self
78 }
79
80 /// Set the data model, overriding the one derived from the architecture and OS. The only
81 /// target in the matrix that needs this is `x86_64-linux-gnux32`.
82 pub const fn data_model(mut self, data_model: DataModel) -> Self {
83 self.data_model = Some(data_model);
84 self
85 }
86
87 /// Set the OS version, which is a deployment target on Darwin and a preview number on WASI.
88 pub const fn os_version(mut self, version: Version) -> Self {
89 self.os_version = Some(version);
90 self
91 }
92
93 /// Set the environment, overriding the OS default.
94 pub const fn env(mut self, env: Env) -> Self {
95 self.env = Some(env);
96 self
97 }
98
99 /// Set the environment version, which is a glibc version or an Android API level.
100 pub const fn env_version(mut self, version: Version) -> Self {
101 self.env_version = Some(version);
102 self
103 }
104
105 /// Set the float ABI.
106 pub const fn abi(mut self, abi: Abi) -> Self {
107 self.abi = abi;
108 self
109 }
110
111 /// Derive the remaining fields and check the combination describes a machine.
112 ///
113 /// # Errors
114 ///
115 /// Returns the specific mismatch, never a generic failure. A caller that cannot say which
116 /// component the user got wrong produces a diagnostic the user cannot act on.
117 pub fn build(self) -> Result<TargetTuple, Error> {
118 let arch = self.arch;
119 let sub_arch = self.sub_arch;
120 if !sub_arch.belongs_to(arch) {
121 return Err(Error::SubArchMismatch { arch, sub_arch });
122 }
123
124 let endian = self.endian.unwrap_or(arch.default_endian());
125 if endian != arch.default_endian() && !arch.has_both_endians() {
126 return Err(Error::EndianUnsupported { arch, endian });
127 }
128
129 let os = self.os;
130 let env = self.env.unwrap_or(os.default_env());
131 if !env_is_valid_for(os, env) {
132 return Err(Error::EnvMismatch { os, env });
133 }
134
135 let abi = self.abi;
136 if !abi.is_valid_for(arch) {
137 return Err(Error::AbiMismatch { arch, abi });
138 }
139
140 if self.os_version.is_some() && !os.takes_version() {
141 return Err(Error::VersionNotAccepted { component: "operating system" });
142 }
143 if self.env_version.is_some() && !env.is_libc() {
144 return Err(Error::VersionNotAccepted { component: "environment" });
145 }
146
147 let data_model = self.data_model.unwrap_or_else(|| default_data_model(arch, os, env));
148 let object_format = os.object_format().unwrap_or(match arch {
149 Arch::Wasm32 => ObjectFormat::Wasm,
150 _ => ObjectFormat::Elf,
151 });
152
153 Ok(TargetTuple {
154 arch,
155 sub_arch,
156 endian,
157 data_model,
158 os,
159 os_version: self.os_version,
160 env,
161 env_version: self.env_version,
162 abi,
163 object_format,
164 })
165 }
166}
167
168/// Whether an OS and an environment go together.
169///
170/// Two rules rather than a table of pairs. A libc is selectable only on Linux, which is the one
171/// system where the user genuinely picks one. The Darwin ABI variants are legal only on Darwin.
172fn env_is_valid_for(os: Os, env: Env) -> bool {
173 match env {
174 Env::None => true,
175 Env::Gnu => matches!(os, Os::Linux | Os::Windows),
176 Env::Musl => matches!(os, Os::Linux),
177 Env::Android => matches!(os, Os::Linux),
178 Env::Msvc => matches!(os, Os::Windows),
179 Env::Simulator | Env::MacAbi => os.is_darwin(),
180 }
181}
182
183/// The widths of `int`, `long` and a pointer for a target that did not name them.
184///
185/// Windows is the whole reason this is not just a function of the architecture. A 64-bit
186/// Windows target has 64-bit pointers and a 32-bit `long`, and a compiler that reads the
187/// pointer width off the architecture and assumes `long` matches it produces structures whose
188/// layout disagrees with every Windows header.
189fn default_data_model(arch: Arch, os: Os, _env: Env) -> DataModel {
190 match arch {
191 Arch::X86 | Arch::Arm | Arch::Riscv32 | Arch::Wasm32 => DataModel::Ilp32,
192 _ => {
193 if matches!(os, Os::Windows) {
194 DataModel::Llp64
195 } else {
196 DataModel::Lp64
197 }
198 }
199 }
200}
201
202impl TargetTuple {
203 /// The common case: an architecture, an OS, and the OS's default environment.
204 ///
205 /// # Errors
206 ///
207 /// Returns the mismatch if the pair does not describe a machine.
208 pub fn new(arch: Arch, os: Os) -> Result<Self, Error> {
209 TupleBuilder::new(arch, os).build()
210 }
211
212 /// Start a builder for a target that needs more than an architecture and an OS.
213 pub const fn builder(arch: Arch, os: Os) -> TupleBuilder {
214 TupleBuilder::new(arch, os)
215 }
216
217 /// The instruction set family.
218 pub const fn arch(self) -> Arch {
219 self.arch
220 }
221
222 /// The baseline within the family.
223 pub const fn sub_arch(self) -> SubArch {
224 self.sub_arch
225 }
226
227 /// The byte order.
228 pub const fn endian(self) -> Endian {
229 self.endian
230 }
231
232 /// The widths of `int`, `long` and a pointer.
233 pub const fn data_model(self) -> DataModel {
234 self.data_model
235 }
236
237 /// The operating system.
238 pub const fn os(self) -> Os {
239 self.os
240 }
241
242 /// The OS version, which is a deployment target on Darwin and a preview number on WASI.
243 pub const fn os_version(self) -> Option<Version> {
244 self.os_version
245 }
246
247 /// The environment, which is the C library on Linux and the ABI variant elsewhere.
248 pub const fn env(self) -> Env {
249 self.env
250 }
251
252 /// The environment version, which is a glibc version or an Android API level.
253 pub const fn env_version(self) -> Option<Version> {
254 self.env_version
255 }
256
257 /// The float ABI as the tuple named it, which is [`Abi::Default`] unless the user was
258 /// explicit. Call [`TargetTuple::resolved_abi`] to get the one code generation uses.
259 pub const fn abi(self) -> Abi {
260 self.abi
261 }
262
263 /// The float ABI with the target's default filled in.
264 pub const fn resolved_abi(self) -> Abi {
265 self.abi.resolve(self.arch, self.sub_arch, self.os, self.env)
266 }
267
268 /// The container the compiler writes for this target.
269 pub const fn object_format(self) -> ObjectFormat {
270 self.object_format
271 }
272
273 /// The width of a pointer in bits, read from the data model rather than from the
274 /// architecture.
275 pub const fn pointer_width(self) -> u32 {
276 self.data_model.pointer_width()
277 }
278
279 /// Whether bytes are stored least significant first.
280 pub const fn is_little_endian(self) -> bool {
281 matches!(self.endian, Endian::Little)
282 }
283
284 /// Whether plain `char` is signed.
285 ///
286 /// Signed on x86 and wasm, unsigned on ARM, AArch64, RISC-V, LoongArch, PowerPC and s390x.
287 /// This is the classic first cross compilation bug, because a corpus written and tested on
288 /// x86-64 contains code that assumes `char` holds negative values and it passes until the day
289 /// it runs on ARM.
290 ///
291 /// s390x is the row worth naming, because the obvious guess is wrong. It is a big-endian
292 /// mainframe architecture with a signed everything else, and its `char` is unsigned, which the
293 /// ELF ABI supplement says and which `__CHAR_UNSIGNED__` from a cross compiler confirms.
294 ///
295 /// The operating system overrides the architecture twice. Windows says signed everywhere
296 /// because the Microsoft ABI does, and Darwin says signed on AArch64 because Apple kept it
297 /// that way for source compatibility with the Intel Macs, against what AAPCS64 says. So
298 /// `aarch64-macos` and `aarch64-linux-gnu` are the same architecture with opposite answers,
299 /// which is the pair most likely to catch a corpus out.
300 pub const fn char_is_signed(self) -> bool {
301 match self.arch {
302 Arch::X86_64 | Arch::X86 | Arch::Wasm32 => true,
303 Arch::Arm | Arch::Aarch64 | Arch::Arm64Ec => {
304 matches!(self.os, Os::Windows) || self.os.is_darwin()
305 }
306 Arch::Riscv64 | Arch::Riscv32 | Arch::LoongArch64 | Arch::PowerPc64 | Arch::S390x => {
307 false
308 }
309 }
310 }
311
312 /// Whether symbols carry a leading underscore.
313 pub const fn leading_underscore(self) -> bool {
314 self.object_format.leading_underscore(self.data_model)
315 }
316
317 /// The leading component of the canonical spelling, which is the architecture with its
318 /// baseline and byte order folded in.
319 ///
320 /// The folding is not decoration. `powerpc64le` and `armv7` and `aarch64_be` are what every
321 /// other toolchain writes, and a tuple that spelled them as separate components would not
322 /// paste into anybody's build script.
323 pub fn arch_component(self) -> String {
324 let mut s = String::new();
325 match self.arch {
326 Arch::PowerPc64 => {
327 s.push_str("powerpc64");
328 if matches!(self.endian, Endian::Little) {
329 s.push_str("le");
330 }
331 }
332 Arch::Arm => {
333 s.push_str("arm");
334 s.push_str(self.sub_arch.as_str());
335 if matches!(self.endian, Endian::Big) {
336 s.push_str("eb");
337 }
338 }
339 Arch::Aarch64 => {
340 s.push_str("aarch64");
341 if matches!(self.endian, Endian::Big) {
342 s.push_str("_be");
343 }
344 }
345 other => {
346 s.push_str(other.as_str());
347 if self.endian != other.default_endian() {
348 s.push_str("eb");
349 }
350 }
351 }
352 s
353 }
354
355 /// The OS component of the canonical spelling, with its version.
356 pub fn os_component(self) -> String {
357 match (self.os, self.os_version) {
358 (Os::Wasi, Some(v)) => format!("wasip{}", v.major_part()),
359 (Os::Wasi, None) => "wasi".to_string(),
360 (os, Some(v)) => format!("{os}.{v}"),
361 (os, None) => os.as_str().to_string(),
362 }
363 }
364
365 /// The environment component of the canonical spelling, with its version and with the
366 /// suffixes GCC fuses into it put back.
367 ///
368 /// Empty when there is nothing to say, and the caller drops the separator in that case, so
369 /// `x86_64-none` has two components and `armv7m-none-eabi` has three.
370 pub fn env_component(self) -> String {
371 self.env_component_with(true)
372 }
373
374 /// The environment component, optionally without the dotted version.
375 ///
376 /// The version is dropped for the LLVM spelling because LLVM has no place to put it: an
377 /// Android API level is part of the environment name there and a glibc version simply is
378 /// not expressible. Emitting `gnu.2.28` into a `.ll` file would produce a triple that LLVM
379 /// parses as an unknown environment, which is worse than losing the pin, and the pin is
380 /// still in the canonical spelling that everything of ours reads.
381 fn env_component_with(self, dotted_version: bool) -> String {
382 let mut s = String::new();
383 match (self.env, self.env_version) {
384 (Env::Android, Some(v)) => {
385 s.push_str("android");
386 s.push_str(&v.major_part().to_string());
387 }
388 (env, Some(v)) if dotted_version => {
389 s.push_str(env.as_str());
390 s.push('.');
391 s.push_str(&v.to_string());
392 }
393 (env, _) => s.push_str(env.as_str()),
394 }
395
396 if matches!(self.arch, Arch::Arm) {
397 match self.resolved_abi() {
398 Abi::DoubleFloat => s.push_str("eabihf"),
399 _ => s.push_str("eabi"),
400 }
401 } else if matches!(self.data_model, DataModel::Ilp32On64) {
402 s.push_str("x32");
403 }
404 s
405 }
406
407 /// The canonical spelling. This is what `--print-target-triple` answers and what the cache
408 /// is keyed on.
409 ///
410 /// It has no vendor component. The vendor field in a GNU triple has carried no information
411 /// since the last vendor that mattered stopped shipping a Unix, and every tool that reads
412 /// one has to special case `unknown`, `pc`, `none` and `w64` to get past it. The parser
413 /// accepts a vendor so that pasted triples work; the canonical form does not write one.
414 pub fn to_canonical_string(self) -> String {
415 let env = self.env_component();
416 if env.is_empty() {
417 format!("{}-{}", self.arch_component(), self.os_component())
418 } else {
419 format!("{}-{}-{}", self.arch_component(), self.os_component(), env)
420 }
421 }
422
423 /// The LLVM spelling, with a vendor and with a three component OS version.
424 ///
425 /// This exists because the tuple has to leave the building. A `.ll` file, an object file's
426 /// target metadata, a `--target` handed to an external tool and a user pasting from a Clang
427 /// invocation all speak LLVM's dialect, and `--print-llvm-triple` is the flag that says what
428 /// it would be. `spec/cross-compile/12-driver.md` section 12.3 keeps the two flags separate rather than
429 /// picking one spelling and making half the users translate.
430 pub fn to_llvm_string(self) -> String {
431 let vendor = match self.os {
432 Os::MacOs | Os::IOs => "apple",
433 Os::Windows => match self.env {
434 Env::Gnu => "w64",
435 _ => "pc",
436 },
437 Os::Illumos => "pc",
438 _ => "unknown",
439 };
440
441 let os = match (self.os, self.os_version) {
442 (Os::MacOs, Some(v)) => format!("macosx{}", v.to_llvm_string()),
443 (Os::MacOs, None) => "macosx".to_string(),
444 (Os::IOs, Some(v)) => format!("ios{}", v.to_llvm_string()),
445 (Os::Wasi, Some(v)) if v.major_part() == 1 => "wasi".to_string(),
446 (Os::Wasi, Some(v)) => format!("wasip{}", v.major_part()),
447 (Os::None, _) => "none".to_string(),
448 (os, _) => os.as_str().to_string(),
449 };
450
451 let env = self.env_component_with(false);
452 if env.is_empty() {
453 format!("{}-{}-{}", self.arch_component(), vendor, os)
454 } else {
455 format!("{}-{}-{}-{}", self.arch_component(), vendor, os, env)
456 }
457 }
458}
459
460impl fmt::Display for TargetTuple {
461 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
462 f.write_str(&self.to_canonical_string())
463 }
464}
465
466impl FromStr for TargetTuple {
467 type Err = Error;
468
469 fn from_str(s: &str) -> Result<Self, Self::Err> {
470 crate::parse::parse(s)
471 }
472}