rucc_target/abi.rs
1//! How an argument travels and how a return value comes back, which is the target's answer
2//! and never C's.
3//!
4//! Design: `spec/12-abi-and-runtime.md` sections 12.1 to 12.5, and
5//! `spec/cross-compile/06-abis.md` sections 6.2 and 6.7.
6//!
7//! The same declaration passes a pair of registers on one target and a hidden pointer on
8//! another, so this is the one question about a C function that cannot be answered by reading
9//! the C. It used to be answered here, by four hand written classifiers covering the four ABIs
10//! this compiler had backends for. It is answered by [`rucc_abi`] now, where the same five ABIs
11//! are data rather than code and are checked against a reference compiler, and this module is
12//! the door between the compiler and that crate.
13//!
14//! # What is asked and what is answered
15//!
16//! A caller flattens a C type into a [`Shape`], which is a size, an alignment and the scalars
17//! inside it with the offsets the layout gave them. That is everything every psABI reads: the
18//! classification rules are all written over where the scalars are and whether they are integers
19//! or floating point. Flattening is the caller's job because it is where the C type system
20//! lives, and every rule after it is the target's.
21//!
22//! The answer is a [`Pass`], which is one of five things: nothing travels, the value travels as
23//! itself, the object travels as a list of [`Slot`]s that each hold a register's worth of it,
24//! the address of a copy travels in its place, or the object's own bytes go in the argument
25//! area. A scalar is always [`Pass::Direct`]: whether it ends up in a register or on the stack
26//! is the backend's arithmetic and not a change of form, and the only reason this cares about
27//! scalars at all is that they spend the registers an aggregate after them was hoping for.
28//!
29//! # Why one call at a time
30//!
31//! Three of these ABIs put an aggregate in memory when the registers it wanted are gone, so the
32//! answer for one argument depends on every argument before it and on whether the return value
33//! took a register on its way past. That is what [`Call`] is: the registers a call has left.
34//! Ask it about the return value first, then about the arguments in order, which is the order
35//! the ABI documents themselves are written in.
36
37use rucc_abi::abis;
38
39#[doc(inline)]
40pub use rucc_abi::{Arg, Call, Kind, Pass, Piece, Scalar, Shape, Slot};
41
42use crate::TargetInfo;
43
44impl TargetInfo {
45 /// The start of one call, with every argument register still to spend, and [`None`] on a
46 /// target whose ABI is not described.
47 ///
48 /// The [`None`] is AArch64 on Windows and nothing else today. That ABI is AAPCS64 with a
49 /// different variadic rule and x18 reserved, per `spec/cross-compile/06-abis.md` section 6.1,
50 /// and it is not written down yet. This used to answer AAPCS64 for it, which is the almost
51 /// right answer, and an almost right ABI is the failure mode `spec/cross-compile/02-the-goal.md`
52 /// exists to rule out: everything builds, everything links, and a structure crosses a
53 /// library boundary with its members in the wrong registers. A caller that cannot compile
54 /// the call says so instead.
55 #[must_use]
56 pub fn call(&self) -> Option<Call> {
57 abis::for_target(self.tuple).map(rucc_abi::AbiDescription::call)
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use rucc_abi::Format;
64
65 use super::*;
66 use crate::Triple;
67
68 /// The target with this triple.
69 fn target(triple: &str) -> TargetInfo {
70 TargetInfo::new(triple.parse::<Triple>().expect("a triple the compiler supports"))
71 }
72
73 /// The pieces of a record whose members are these, each at the next offset it fits.
74 fn packed(scalars: &[Scalar]) -> Vec<Piece> {
75 let mut pieces = Vec::new();
76 let mut at: u64 = 0;
77 for &scalar in scalars {
78 at = at.next_multiple_of(scalar.align.max(1));
79 pieces.push(Piece { offset: at, scalar });
80 at += scalar.size;
81 }
82 pieces
83 }
84
85 /// The shape of a record whose members are these, sized and aligned the way C would.
86 fn record<'a>(pieces: &'a [Piece]) -> Shape<'a> {
87 let align = pieces.iter().map(|piece| piece.scalar.align).max().unwrap_or(1);
88 let size = pieces.iter().map(Piece::end).max().unwrap_or(0).next_multiple_of(align);
89 Shape { size, align, pieces, complex: false }
90 }
91
92 #[test]
93 fn a_triple_picks_the_abi_and_not_the_architecture_alone() {
94 let mut linux = target("x86_64-unknown-linux-gnu").call().expect("a described ABI");
95 let mut windows = target("x86_64-pc-windows-msvc").call().expect("a described ABI");
96 let pieces = packed(&[Scalar::integer(8), Scalar::integer(8)]);
97 let shape = Arg::Aggregate(record(&pieces));
98 // Sixteen bytes is two registers on SysV and a hidden pointer on Windows, which is the
99 // whole reason this is data about the target rather than a rule about C.
100 assert_eq!(
101 linux.argument(&shape),
102 Pass::Pieces(vec![
103 Slot::Integer { offset: 0, size: 8 },
104 Slot::Integer { offset: 8, size: 8 },
105 ])
106 );
107 assert_eq!(windows.argument(&shape), Pass::Reference);
108 }
109
110 #[test]
111 fn a_homogeneous_floating_point_aggregate_travels_in_vector_registers() {
112 let mut call = target("aarch64-unknown-linux-gnu").call().expect("a described ABI");
113 let pieces = packed(&[Scalar::float(Format::Single, 4); 3]);
114 let shape = Arg::Aggregate(record(&pieces));
115 // Three `float`s are three vector registers on AAPCS64, and adding anything that is not
116 // a `float` makes the whole thing an eightbyte pair in general purpose registers.
117 assert_eq!(
118 call.argument(&shape),
119 Pass::Pieces(vec![
120 Slot::Float { offset: 0, format: Format::Single },
121 Slot::Float { offset: 4, format: Format::Single },
122 Slot::Float { offset: 8, format: Format::Single },
123 ])
124 );
125 }
126
127 #[test]
128 fn the_two_darwin_targets_follow_different_abis() {
129 // Apple's arm64 is its own description rather than AAPCS64 with a note, because its
130 // variadic rule and its `long double` both differ. x86-64 Darwin is plain SysV.
131 let arm = target("aarch64-apple-darwin").call().expect("a described ABI");
132 let intel = target("x86_64-apple-darwin").call().expect("a described ABI");
133 assert_eq!(arm.abi().name, "Darwin arm64");
134 assert_eq!(intel.abi().name, "SysV AMD64");
135 }
136
137 #[test]
138 fn aarch64_on_windows_has_no_answer_rather_than_an_almost_right_one() {
139 // The one target the compiler can name and cannot classify a call for. It is AAPCS64
140 // with a different variadic rule and x18 reserved, and until that is written down
141 // saying so is the only honest answer.
142 assert!(target("aarch64-pc-windows-msvc").call().is_none());
143 assert!(target("aarch64-unknown-linux-gnu").call().is_some());
144 assert!(target("x86_64-pc-windows-msvc").call().is_some());
145 }
146
147 #[test]
148 fn every_other_triple_the_compiler_accepts_can_classify_a_call() {
149 use crate::{Arch, Env, Os};
150
151 let mut described = 0;
152 for arch in [Arch::X86_64, Arch::Aarch64, Arch::Riscv64] {
153 for os in [Os::Linux, Os::Darwin, Os::Windows, Os::None] {
154 for env in [Env::None, Env::Gnu, Env::Musl, Env::Msvc] {
155 let target = TargetInfo::new(Triple { arch, os, env });
156 // AArch64 on Windows is the only gap, and it is one gap rather than a family
157 // of them: a target with no operating system still has a calling convention,
158 // because a freestanding program calls functions.
159 let expected = !(arch == Arch::Aarch64 && os == Os::Windows);
160 assert_eq!(
161 target.call().is_some(),
162 expected,
163 "{arch:?} {os:?} {env:?} disagrees about whether its ABI is described"
164 );
165 described += usize::from(target.call().is_some());
166 }
167 }
168 }
169 // Forty eight combinations the triple can hold, less the four spellings of AArch64 on
170 // Windows, which are one gap rather than four because the environment does not reach the
171 // choice of ABI on that pair.
172 assert_eq!(described, 44);
173 }
174}