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 same target with `version` as its OS version. A deployment target given apart from
248 /// the tuple, the way `-mmacosx-version-min=` gives one, joins it here.
249 #[must_use]
250 pub const fn with_os_version(mut self, version: Version) -> Self {
251 self.os_version = Some(version);
252 self
253 }
254
255 /// The same target with no OS or environment version, which is the machine and the ABI
256 /// without the release of either that the program is promised to run on.
257 #[must_use]
258 pub const fn without_versions(mut self) -> Self {
259 self.os_version = None;
260 self.env_version = None;
261 self
262 }
263
264 /// The environment, which is the C library on Linux and the ABI variant elsewhere.
265 pub const fn env(self) -> Env {
266 self.env
267 }
268
269 /// The environment version, which is a glibc version or an Android API level.
270 pub const fn env_version(self) -> Option<Version> {
271 self.env_version
272 }
273
274 /// The float ABI as the tuple named it, which is [`Abi::Default`] unless the user was
275 /// explicit. Call [`TargetTuple::resolved_abi`] to get the one code generation uses.
276 pub const fn abi(self) -> Abi {
277 self.abi
278 }
279
280 /// The float ABI with the target's default filled in.
281 pub const fn resolved_abi(self) -> Abi {
282 self.abi.resolve(self.arch, self.sub_arch, self.os, self.env)
283 }
284
285 /// The container the compiler writes for this target.
286 pub const fn object_format(self) -> ObjectFormat {
287 self.object_format
288 }
289
290 /// The width of a pointer in bits, read from the data model rather than from the
291 /// architecture.
292 pub const fn pointer_width(self) -> u32 {
293 self.data_model.pointer_width()
294 }
295
296 /// Whether bytes are stored least significant first.
297 pub const fn is_little_endian(self) -> bool {
298 matches!(self.endian, Endian::Little)
299 }
300
301 /// Whether plain `char` is signed.
302 ///
303 /// Signed on x86 and wasm, unsigned on ARM, AArch64, RISC-V, LoongArch, PowerPC and s390x.
304 /// This is the classic first cross compilation bug, because a corpus written and tested on
305 /// x86-64 contains code that assumes `char` holds negative values and it passes until the day
306 /// it runs on ARM.
307 ///
308 /// s390x is the row worth naming, because the obvious guess is wrong. It is a big-endian
309 /// mainframe architecture with a signed everything else, and its `char` is unsigned, which the
310 /// ELF ABI supplement says and which `__CHAR_UNSIGNED__` from a cross compiler confirms.
311 ///
312 /// The operating system overrides the architecture twice. Windows says signed everywhere
313 /// because the Microsoft ABI does, and Darwin says signed on AArch64 because Apple kept it
314 /// that way for source compatibility with the Intel Macs, against what AAPCS64 says. So
315 /// `aarch64-macos` and `aarch64-linux-gnu` are the same architecture with opposite answers,
316 /// which is the pair most likely to catch a corpus out.
317 pub const fn char_is_signed(self) -> bool {
318 match self.arch {
319 Arch::X86_64 | Arch::X86 | Arch::Wasm32 => true,
320 Arch::Arm | Arch::Aarch64 | Arch::Arm64Ec => {
321 matches!(self.os, Os::Windows) || self.os.is_darwin()
322 }
323 Arch::Riscv64 | Arch::Riscv32 | Arch::LoongArch64 | Arch::PowerPc64 | Arch::S390x => {
324 false
325 }
326 }
327 }
328
329 /// Whether symbols carry a leading underscore.
330 pub const fn leading_underscore(self) -> bool {
331 self.object_format.leading_underscore(self.data_model)
332 }
333
334 /// The leading component of the canonical spelling, which is the architecture with its
335 /// baseline and byte order folded in.
336 ///
337 /// The folding is not decoration. `powerpc64le` and `armv7` and `aarch64_be` are what every
338 /// other toolchain writes, and a tuple that spelled them as separate components would not
339 /// paste into anybody's build script.
340 pub fn arch_component(self) -> String {
341 let mut s = String::new();
342 match self.arch {
343 Arch::PowerPc64 => {
344 s.push_str("powerpc64");
345 if matches!(self.endian, Endian::Little) {
346 s.push_str("le");
347 }
348 }
349 Arch::Arm => {
350 s.push_str("arm");
351 s.push_str(self.sub_arch.as_str());
352 if matches!(self.endian, Endian::Big) {
353 s.push_str("eb");
354 }
355 }
356 Arch::Aarch64 => {
357 s.push_str("aarch64");
358 if matches!(self.endian, Endian::Big) {
359 s.push_str("_be");
360 }
361 }
362 other => {
363 s.push_str(other.as_str());
364 if self.endian != other.default_endian() {
365 s.push_str("eb");
366 }
367 }
368 }
369 s
370 }
371
372 /// The OS component of the canonical spelling, with its version.
373 pub fn os_component(self) -> String {
374 match (self.os, self.os_version) {
375 (Os::Wasi, Some(v)) => format!("wasip{}", v.major_part()),
376 (Os::Wasi, None) => "wasi".to_string(),
377 (os, Some(v)) => format!("{os}.{v}"),
378 (os, None) => os.as_str().to_string(),
379 }
380 }
381
382 /// The environment component of the canonical spelling, with its version and with the
383 /// suffixes GCC fuses into it put back.
384 ///
385 /// Empty when there is nothing to say, and the caller drops the separator in that case, so
386 /// `x86_64-none` has two components and `armv7m-none-eabi` has three.
387 pub fn env_component(self) -> String {
388 self.env_component_with(true)
389 }
390
391 /// The environment component, optionally without the dotted version.
392 ///
393 /// The version is dropped for the LLVM spelling because LLVM has no place to put it: an
394 /// Android API level is part of the environment name there and a glibc version simply is
395 /// not expressible. Emitting `gnu.2.28` into a `.ll` file would produce a triple that LLVM
396 /// parses as an unknown environment, which is worse than losing the pin, and the pin is
397 /// still in the canonical spelling that everything of ours reads.
398 fn env_component_with(self, dotted_version: bool) -> String {
399 let mut s = String::new();
400 match (self.env, self.env_version) {
401 (Env::Android, Some(v)) => {
402 s.push_str("android");
403 s.push_str(&v.major_part().to_string());
404 }
405 (env, Some(v)) if dotted_version => {
406 s.push_str(env.as_str());
407 s.push('.');
408 s.push_str(&v.to_string());
409 }
410 (env, _) => s.push_str(env.as_str()),
411 }
412
413 if matches!(self.arch, Arch::Arm) {
414 match self.resolved_abi() {
415 Abi::DoubleFloat => s.push_str("eabihf"),
416 _ => s.push_str("eabi"),
417 }
418 } else if matches!(self.data_model, DataModel::Ilp32On64) {
419 s.push_str("x32");
420 }
421 s
422 }
423
424 /// The canonical spelling. This is what `--print-target-triple` answers and what the cache
425 /// is keyed on.
426 ///
427 /// It has no vendor component. The vendor field in a GNU triple has carried no information
428 /// since the last vendor that mattered stopped shipping a Unix, and every tool that reads
429 /// one has to special case `unknown`, `pc`, `none` and `w64` to get past it. The parser
430 /// accepts a vendor so that pasted triples work; the canonical form does not write one.
431 pub fn to_canonical_string(self) -> String {
432 let env = self.env_component();
433 if env.is_empty() {
434 format!("{}-{}", self.arch_component(), self.os_component())
435 } else {
436 format!("{}-{}-{}", self.arch_component(), self.os_component(), env)
437 }
438 }
439
440 /// The LLVM spelling, with a vendor and with a three component OS version.
441 ///
442 /// This exists because the tuple has to leave the building. A `.ll` file, an object file's
443 /// target metadata, a `--target` handed to an external tool and a user pasting from a Clang
444 /// invocation all speak LLVM's dialect, and `--print-llvm-triple` is the flag that says what
445 /// it would be. `spec/cross-compile/12-driver.md` section 12.3 keeps the two flags separate rather than
446 /// picking one spelling and making half the users translate.
447 pub fn to_llvm_string(self) -> String {
448 let vendor = match self.os {
449 Os::MacOs | Os::IOs => "apple",
450 Os::Windows => match self.env {
451 Env::Gnu => "w64",
452 _ => "pc",
453 },
454 Os::Illumos => "pc",
455 _ => "unknown",
456 };
457
458 let os = match (self.os, self.os_version) {
459 (Os::MacOs, Some(v)) => format!("macosx{}", v.to_llvm_string()),
460 (Os::MacOs, None) => "macosx".to_string(),
461 (Os::IOs, Some(v)) => format!("ios{}", v.to_llvm_string()),
462 (Os::Wasi, Some(v)) if v.major_part() == 1 => "wasi".to_string(),
463 (Os::Wasi, Some(v)) => format!("wasip{}", v.major_part()),
464 (Os::None, _) => "none".to_string(),
465 (os, _) => os.as_str().to_string(),
466 };
467
468 let env = self.env_component_with(false);
469 if env.is_empty() {
470 format!("{}-{}-{}", self.arch_component(), vendor, os)
471 } else {
472 format!("{}-{}-{}-{}", self.arch_component(), vendor, os, env)
473 }
474 }
475}
476
477impl fmt::Display for TargetTuple {
478 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
479 f.write_str(&self.to_canonical_string())
480 }
481}
482
483impl FromStr for TargetTuple {
484 type Err = Error;
485
486 fn from_str(s: &str) -> Result<Self, Self::Err> {
487 crate::parse::parse(s)
488 }
489}