rucc_abi/layout.rs
1//! Type sizes, alignments and signedness, which is the half of a psABI that decides whether a
2//! header is read correctly.
3//!
4//! Design: `spec/cross-compile/06-abis.md` section 6.2 items 1 to 3.
5//!
6//! Section 6.2 splits a psABI into twelve independent decisions and then splits those into two
7//! groups. Items 4 to 9 decide whether a call works, and the differential harness of
8//! `spec/cross-compile/14-testing.md` is what tests them. Items 1 to 3 plus item 11 decide whether a *header*
9//! is interpreted correctly, and those are this file.
10//!
11//! The second group is cheaper to test and catches more, which is why `spec/cross-compile/06-abis.md` section
12//! 6.8 puts the `_Static_assert` corpus first for every new ABI: it costs nothing to run, needs
13//! no target machine and no execution, and it catches every layout disagreement there is. The
14//! corpus is generated from this description, which is why the description comes first.
15//!
16//! # The two traps
17//!
18//! A `long double`'s width and its format are separate facts. Sixteen bytes on x86-64 Linux of
19//! which eighty bits are the value, eight bytes and a plain `double` on Darwin and under MSVC,
20//! IEEE binary128 on AArch64 Linux and s390x and RISC-V, and IBM double-double on legacy
21//! 64-bit PowerPC. A description that carried only the width would call three of those the same.
22//!
23//! Windows is two answers and not one. mingw-w64 keeps GCC's eighty bit `long double` and MSVC
24//! makes it a `double`, so the rule reads the environment and not the operating system.
25//!
26//! `char`'s signedness is a target fact and not a C fact. Unsigned on AArch64 Linux, on ARM, on
27//! PowerPC and on s390x, signed on x86 and on Darwin. A program that indexes an array with a
28//! `char` holding a byte over 127 works on one and not the other, and nothing in it is wrong.
29
30use rucc_tuple::{Arch, DataModel, Env, Os, TargetTuple};
31
32use crate::shape::Format;
33
34/// A floating point type, as the two separate facts it is.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct FloatType {
37 /// How the bits mean a number.
38 pub format: Format,
39 /// How many bytes it takes in memory, which is not the format's width.
40 pub size: u64,
41 /// What it is aligned to, in bytes, which is not always its size: an x87 `long double` is
42 /// twelve bytes aligned to four on i386 and sixteen aligned to sixteen on x86-64, and it is
43 /// the same eighty bits of value both times.
44 pub align: u64,
45}
46
47/// How a bit-field is allocated within its storage unit.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum BitfieldOrder {
50 /// The first field declared gets the low order bits, which is every little-endian target
51 /// here.
52 LowestFirst,
53 /// The first field declared gets the high order bits, which is s390x and every other
54 /// big-endian ELF target.
55 HighestFirst,
56}
57
58/// The type sizes and alignments a target's headers were written against.
59///
60/// Everything here is a fact the psABI states and the compiler has to agree with. None of it is
61/// something C decides, which is the point: two compilers can both implement C correctly and
62/// disagree about every field in this struct, and the one that disagrees with the target's
63/// headers is the one that is wrong.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct DataLayout {
66 /// Whether a plain `char` is signed.
67 pub char_is_signed: bool,
68 /// The width of a `short` in bytes, which is two everywhere on the target list.
69 pub short_size: u64,
70 /// The width of an `int` in bytes.
71 pub int_size: u64,
72 /// The width of a `long` in bytes, which is the field the old three field triple got wrong
73 /// for every 64-bit Windows target.
74 pub long_size: u64,
75 /// The width of a `long long` in bytes.
76 pub long_long_size: u64,
77 /// What a `long long` is aligned to.
78 ///
79 /// Four on System V i386, where it is eight bytes aligned to four, which is
80 /// `spec/cross-compile/06-abis.md` section 6.2 item 2's example of a layout rule that is not
81 /// derivable from the member alignments. Eight under mingw on the same architecture.
82 pub long_long_align: u64,
83 /// The width of a pointer in bytes.
84 pub pointer_size: u64,
85 /// What a pointer is aligned to.
86 pub pointer_align: u64,
87 /// `float`.
88 pub float: FloatType,
89 /// `double`.
90 pub double: FloatType,
91 /// `long double`, which is the one that differs across almost every target.
92 pub long_double: FloatType,
93 /// The width of a `wchar_t` in bytes, which decides what a wide string literal is encoded in.
94 ///
95 /// Two on Windows, so a wide string there is UTF-16 and a character outside the basic plane
96 /// takes two elements, and four everywhere else, where it is UTF-32 and no character takes
97 /// more than one. It is the operating system's answer and not the architecture's, which
98 /// `facts/aarch64-windows-msvc.facts` and `facts/aarch64-linux-gnu.facts` show as a pair.
99 pub wchar_size: u64,
100 /// Whether a `wchar_t` is signed.
101 ///
102 /// A separate fact from [`DataLayout::char_is_signed`] and not derivable from it. AArch64
103 /// FreeBSD makes both unsigned, AArch64 NetBSD makes `char` unsigned and `wchar_t` signed, and
104 /// Windows makes `char` signed and `wchar_t` unsigned, so no rule over one of them answers the
105 /// other. `L'\xffffffff'` is minus one where this is true and four billion where it is false.
106 pub wchar_is_signed: bool,
107 /// Whether a symbol gets a leading underscore, which is section 6.2 item 11.
108 pub leading_underscore: bool,
109 /// Which end of a storage unit a bit-field starts at.
110 pub bitfield_order: BitfieldOrder,
111 /// The largest alignment the ABI will give a struct member on its own, in bytes, and [`None`]
112 /// where there is no cap.
113 ///
114 /// Four on System V i386 and eight on s390x. A cap is invisible until a program uses a
115 /// sixteen byte type inside a struct, and then it is a layout difference rather than an
116 /// error.
117 pub max_field_align: Option<u64>,
118 /// Whether the target has `__int128`.
119 ///
120 /// It is a fact about the architecture and not about the pointer width, which is the trap:
121 /// `x86_64-linux-gnux32` has four byte pointers and the type, and `i686-linux-gnu` has four
122 /// byte pointers and not the type. Both references provide it wherever the machine has sixty
123 /// four bit registers to hold half of it in, and refuse it everywhere else.
124 pub has_int128: bool,
125}
126
127impl DataLayout {
128 /// The layout for this target.
129 ///
130 /// Every field is derived from the tuple rather than from the host, which is
131 /// `spec/cross-compile/08-sysroots.md` section 8.5's rule applied to types instead of to directories, and
132 /// it is what makes `spec/cross-compile/02-the-goal.md` claim 5 checkable: two hosts asking about the same
133 /// target get the same answer because there is nothing in here for the host to influence.
134 #[must_use]
135 pub fn for_target(target: TargetTuple) -> Self {
136 let model = target.data_model();
137 let pointer = u64::from(target.pointer_width()) / 8;
138 Self {
139 char_is_signed: target.char_is_signed(),
140 short_size: 2,
141 int_size: u64::from(model.int_width()) / 8,
142 long_size: u64::from(model.long_width()) / 8,
143 long_long_size: u64::from(model.long_long_width()) / 8,
144 long_long_align: long_long_align(target),
145 pointer_size: pointer,
146 pointer_align: pointer,
147 float: FloatType { format: Format::Single, size: 4, align: 4 },
148 double: FloatType { format: Format::Double, size: 8, align: double_align(target) },
149 long_double: long_double(target),
150 wchar_size: match target.os() {
151 Os::Windows => 2,
152 _ => 4,
153 },
154 wchar_is_signed: wchar_is_signed(target),
155 leading_underscore: target.leading_underscore(),
156 bitfield_order: match target.is_little_endian() {
157 true => BitfieldOrder::LowestFirst,
158 false => BitfieldOrder::HighestFirst,
159 },
160 max_field_align: max_field_align(target),
161 has_int128: has_int128(target),
162 }
163 }
164
165 /// Whether a `long double` is really a `double`, which is true on Darwin, under MSVC and on
166 /// every 32-bit ARM target, and which decides whether `%Lf` and `LDBL_MAX` and the `l`
167 /// suffixed math functions mean anything different from their unsuffixed forms.
168 #[must_use]
169 pub const fn long_double_is_double(&self) -> bool {
170 matches!(self.long_double.format, Format::Double)
171 }
172}
173
174/// What a `long long` is aligned to.
175fn long_long_align(target: TargetTuple) -> u64 {
176 // System V i386 is the only target on the list where an eight byte type is aligned to four,
177 // and it is the reason a struct holding one lays out differently there than the member sizes
178 // suggest. mingw is not that target. It follows Microsoft here and aligns to eight, so the
179 // arm has to name the operating system as well as the architecture.
180 match (target.arch(), target.data_model()) {
181 (Arch::X86, DataModel::Ilp32) if target.os() != Os::Windows => 4,
182 _ => 8,
183 }
184}
185
186/// What a `double` is aligned to.
187fn double_align(target: TargetTuple) -> u64 {
188 match (target.arch(), target.data_model()) {
189 // Same rule as `long long`, the same reason, and the same mingw exception. A `double`
190 // inside a struct on System V i386 sits at a four byte boundary, which is why an i386
191 // struct is often smaller than the same declaration on any other target.
192 (Arch::X86, DataModel::Ilp32) if target.os() != Os::Windows => 4,
193 _ => 8,
194 }
195}
196
197/// The `long double` of this target, as a format and a width, which are separate facts.
198fn long_double(target: TargetTuple) -> FloatType {
199 let double = FloatType { format: Format::Double, size: 8, align: 8 };
200 let quad = FloatType { format: Format::Quad, size: 16, align: 16 };
201 match target.arch() {
202 // Eighty bits of value in twelve bytes on i386 and sixteen on x86-64, aligned to its
203 // storage size both times, and the same format underneath.
204 //
205 // MSVC is the exception and it is handled first, because there a `long double` is a
206 // `double` and the x87 format never appears in an interface. It is the environment and
207 // not the operating system that decides this. mingw-w64 keeps the GCC answer, so
208 // `x86_64-windows-gnu` has an eighty bit `long double` in sixteen bytes and
209 // `x86_64-windows-msvc` has a `double` in eight, which is the same OS with two answers
210 // and the reason this arm cannot be written as `os() == Windows`.
211 Arch::X86_64 | Arch::X86 if target.env() == Env::Msvc => double,
212 Arch::X86_64 => FloatType { format: Format::X87Extended, size: 16, align: 16 },
213 // Four on i386, under mingw as well as under System V, which is worth stating because
214 // mingw does follow Microsoft on the alignment of `double` and `long long` and the guess
215 // that it does the same here is wrong.
216 Arch::X86 => FloatType { format: Format::X87Extended, size: 12, align: 4 },
217 // Darwin's third divergence, `spec/cross-compile/06-abis.md` section 6.3. `%Lf` disagrees, `LDBL_MAX`
218 // is wrong, and a math library call resolves to a differently named symbol, all from one
219 // field.
220 Arch::Aarch64 if target.os().is_darwin() => double,
221 Arch::Aarch64 if target.os() == Os::Windows => double,
222 // Sixteen bytes of IEEE quad aligned to eight, because the s390x ELF ABI caps scalar
223 // alignment at eight and a `long double` is the widest scalar it has. Same format as the
224 // arm below and a different alignment, which is why it is a row of its own.
225 Arch::S390x => FloatType { format: Format::Quad, size: 16, align: 8 },
226 // wasm32 is in here and not in with the other thirty two bit machines, which is the
227 // surprise. It has four byte pointers and a sixteen byte IEEE quad `long double` aligned
228 // to sixteen, the same way it has four byte pointers and an `__int128`: the width of an
229 // address on that machine says nothing about the widest type, because there is no register
230 // file for either of them to be a fact about. `facts/wasm32-none.facts` recorded quad and
231 // sixteen from the start and this arm said `double` anyway, which the generated record
232 // corpus caught the first time it was written for the row.
233 // RISC-V 32 is in here for the same reason and it is not in the table, so it was measured
234 // by running `zig cc -target riscv32-linux-musl` rather than assumed from the width: the
235 // RISC-V psABI gives both ILP32 and LP64 a binary128 `long double`, and that machine has
236 // a sixteen byte float and no `__int128` at the same time.
237 Arch::Aarch64 | Arch::Riscv64 | Arch::LoongArch64 | Arch::Wasm32 | Arch::Riscv32 => quad,
238 // Thirty two bit ARM has never had anything wider than a `double` for it, which the four
239 // ARM rows of the table agree about.
240 Arch::Arm => double,
241 // ELFv2 keeps IBM double-double, a pair of `double`s whose sum is the value, which is
242 // not an IEEE format at all and is the reason a `long double` there cannot be treated as
243 // a wide binary float.
244 Arch::PowerPc64 => FloatType { format: Format::DoubleDouble, size: 16, align: 16 },
245 Arch::Arm64Ec => double,
246 }
247}
248
249/// Whether a `wchar_t` is signed.
250///
251/// Three rules over the facts in tamnd/rucc-cross rather than a guess from the architecture. The
252/// ARM family is the only one that makes it unsigned, and two operating systems override that:
253/// Windows makes it an `unsigned short` on every architecture, and Darwin and NetBSD both make it
254/// a plain `int`. `facts/aarch64-freebsd.facts` and `facts/aarch64-netbsd.facts` are the same
255/// architecture with opposite answers, which is why the operating system has to be read here.
256fn wchar_is_signed(target: TargetTuple) -> bool {
257 match target.os() {
258 // A `wchar_t` on Windows is an `unsigned short`, which is also why it is two bytes.
259 Os::Windows => false,
260 // Apple kept the Intel answer on AArch64, the same way it kept plain `char` signed, and
261 // NetBSD makes it an `int` everywhere on purpose.
262 Os::MacOs | Os::IOs | Os::NetBsd => true,
263 _ => !matches!(target.arch(), Arch::Arm | Arch::Aarch64 | Arch::Arm64Ec),
264 }
265}
266
267/// The largest alignment the ABI gives a member on its own.
268fn max_field_align(target: TargetTuple) -> Option<u64> {
269 match (target.arch(), target.data_model()) {
270 // i386 Linux caps member alignment at four, so a sixteen byte aligned type inside a
271 // struct is aligned to four there and to sixteen everywhere else.
272 (Arch::X86, DataModel::Ilp32) if target.os() != Os::Windows => Some(4),
273 // s390x caps at eight, which is why its `long double` and its `__int128` are both sixteen
274 // bytes aligned to eight. Two architectures with a cap and two different caps, which is
275 // the argument for this being a number rather than a boolean.
276 (Arch::S390x, _) => Some(8),
277 _ => None,
278 }
279}
280
281/// Whether the target has `__int128`.
282fn has_int128(target: TargetTuple) -> bool {
283 match target.arch() {
284 // Sixty four bit registers, so the type is a pair of them and both references have it.
285 // x32 is on this side of the line with its four byte pointers, which is the reason the
286 // question is asked of the architecture rather than of the data model.
287 Arch::X86_64
288 | Arch::Aarch64
289 | Arch::Arm64Ec
290 | Arch::Riscv64
291 | Arch::LoongArch64
292 | Arch::PowerPc64
293 | Arch::S390x => true,
294 // wasm32 has it too, which is the second half of the same point: the machine's values are
295 // sixty four bits wide and its addresses are thirty two.
296 Arch::Wasm32 => true,
297 Arch::X86 | Arch::Arm | Arch::Riscv32 => false,
298 }
299}