lib/choir/src/backends/x64/abi.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const artifact = @import("../root.zig").artifact;
3 const registers = @import("registers/root.zig");
4 const encoding = @import("encoding.zig");
5
6 const GPR = registers.GPR;
7 const XMM = registers.XMM;
8
9 pub const max_int_arg_regs = 6;
10
11 pub const max_fp_arg_regs = 8;
12
13 pub const int_arg_regs = [_]GPR{ .rdi, .rsi, .rdx, .rcx, .r8, .r9 };
14
15 pub const fp_arg_regs = [_]XMM{ .xmm0, .xmm1, .xmm2, .xmm3, .xmm4, .xmm5, .xmm6, .xmm7 };
16
17 pub const int_return_reg = GPR.rax;
18
19 /// Largest result count one function, call, or return carries.
20 /// Bounds a result record to 128 bytes; wider products travel through memory references.
21 pub const max_result_count: usize = 16;
22
23 /// Largest result count returned in registers; more results travel through a caller record.
24 pub const max_register_results: usize = 2;
25
26 /// Integer-class result registers in SysV assignment order.
27 pub const int_return_regs = [max_register_results]GPR{ .rax, .rdx };
28
29 /// Floating-point result registers in SysV assignment order.
30 pub const fp_return_regs = [max_register_results]XMM{ .xmm0, .xmm1 };
31
32 pub const fp_return_reg = XMM.xmm0;
33
34 pub const stack_alignment = 16;
35
36 pub const stack_slot_size = 8;
37
38 pub const callee_saved_gprs = registers.callee_saved_gprs;
39
40 pub const PassingKind = enum {
41 int_reg,
42 fp_reg,
43 stack,
44 };
45
46 pub const ValueLocation = union(PassingKind) {
47 int_reg: GPR,
48 fp_reg: XMM,
49 stack: i32,
50 };
51
52 pub const TypeClass = enum {
53 integer,
54 floating_point,
55 };
56
57 pub fn classifyType(type_name: []const u8) TypeClass {
58 if (std.mem.eql(u8, type_name, "arith.i8") or
59 std.mem.eql(u8, type_name, "arith.i16") or
60 std.mem.eql(u8, type_name, "arith.i32") or
61 std.mem.eql(u8, type_name, "arith.i64") or
62 std.mem.eql(u8, type_name, "arith.u8") or
63 std.mem.eql(u8, type_name, "arith.u16") or
64 std.mem.eql(u8, type_name, "arith.u32") or
65 std.mem.eql(u8, type_name, "arith.u64") or
66 std.mem.eql(u8, type_name, "arith.index") or
67 std.mem.eql(u8, type_name, "arith.bool"))
68 {
69 return .integer;
70 }
71
72 if (std.mem.eql(u8, type_name, "arith.f16") or
73 std.mem.eql(u8, type_name, "arith.bf16") or
74 std.mem.eql(u8, type_name, "arith.f32") or
75 std.mem.eql(u8, type_name, "arith.f64"))
76 {
77 return .floating_point;
78 }
79
80 return .integer;
81 }
82
83 pub fn computeArgLocations(arg_types: []const []const u8, locations: []ValueLocation) void {
84 std.debug.assert(locations.len >= arg_types.len);
85
86 var next_int_reg: usize = 0;
87 var next_fp_reg: usize = 0;
88 var stack_offset: i32 = 0;
89
90 for (arg_types, 0..) |type_name, i| {
91 const class = classifyType(type_name);
92 switch (class) {
93 .integer => {
94 if (next_int_reg < max_int_arg_regs) {
95 locations[i] = .{ .int_reg = int_arg_regs[next_int_reg] };
96 next_int_reg += 1;
97 } else {
98 locations[i] = .{ .stack = stack_offset };
99 stack_offset += stack_slot_size;
100 }
101 },
102 .floating_point => {
103 if (next_fp_reg < max_fp_arg_regs) {
104 locations[i] = .{ .fp_reg = fp_arg_regs[next_fp_reg] };
105 next_fp_reg += 1;
106 } else {
107 locations[i] = .{ .stack = stack_offset };
108 stack_offset += stack_slot_size;
109 }
110 },
111 }
112 }
113 }
114
115 pub fn computeCallStackPadding(stack_arg_bytes: u32, save_bytes: u32, call_aligned: bool) u32 {
116 _ = call_aligned;
117 const total = stack_arg_bytes + save_bytes;
118 const rem = total % stack_alignment;
119 if (rem == 0) return 0;
120 return stack_alignment - rem;
121 }
122
123 pub fn computeReturnLocation(type_name: []const u8) ValueLocation {
124 return switch (classifyType(type_name)) {
125 .integer => .{ .int_reg = int_return_reg },
126 .floating_point => .{ .fp_reg = fp_return_reg },
127 };
128 }
129
130 /// Reports whether results travel through a caller-provided record instead of registers.
131 /// Results form a struct whose fields each occupy one eightbyte; SysV returns a struct
132 /// wider than two eightbytes in memory, with the record address as the first argument.
133 pub fn returnsThroughRecord(result_count: usize) bool {
134 return result_count > max_register_results;
135 }
136
137 /// Assigns each register-returned result the next register of its SysV class.
138 pub fn computeReturnLocations(result_types: []const []const u8, locations: []ValueLocation) void {
139 std.debug.assert(result_types.len <= max_register_results);
140 std.debug.assert(locations.len >= result_types.len);
141
142 var next_int_reg: usize = 0;
143 var next_fp_reg: usize = 0;
144 for (result_types, 0..) |type_name, i| {
145 switch (classifyType(type_name)) {
146 .integer => {
147 locations[i] = .{ .int_reg = int_return_regs[next_int_reg] };
148 next_int_reg += 1;
149 },
150 .floating_point => {
151 locations[i] = .{ .fp_reg = fp_return_regs[next_fp_reg] };
152 next_fp_reg += 1;
153 },
154 }
155 }
156 std.debug.assert(next_int_reg + next_fp_reg == result_types.len);
157 }
158
159 /// Returns the byte offset of one result inside a caller-provided result record.
160 pub fn recordFieldOffset(index: usize) i32 {
161 std.debug.assert(index < max_result_count);
162 return @intCast(index * stack_slot_size);
163 }
164
165 comptime {
166 std.debug.assert(artifact.Signature.max_results == max_result_count);
167 }
168
169 /// Reports whether `signature` is valid and this backend passes and returns every type in it.
170 /// Parameters must be scalars or memrefs, and a vector result must be the only result.
171 pub fn admitsSignature(signature: *const artifact.Signature) bool {
172 if (!signature.isValid()) return false;
173 for (signature.parameters()) |parameter| {
174 if (parameter == .vector) return false;
175 }
176 const results = signature.results();
177 if (results.len < 2) return true;
178 for (results) |result| {
179 if (result == .vector) return false;
180 }
181 return true;
182 }
183
184 pub const FrameLayout = struct {
185 stack_alloc: u32,
186 padding: u32,
187 num_callee_saved: u8,
188 used_callee_saved: [callee_saved_gprs.len]GPR,
189 call_aligned: bool,
190
191 pub fn emitPrologue(self: FrameLayout, allocator: std.mem.Allocator, out: *std.ArrayListUnmanaged(u8)) !void {
192 try appendEncoding(out, allocator, encoding.pushReg(.rbp));
193 try appendEncoding(out, allocator, encoding.movRegReg(.rbp, .rsp));
194
195 var i: usize = 0;
196 while (i < self.num_callee_saved) : (i += 1) {
197 try appendEncoding(out, allocator, encoding.pushReg(self.used_callee_saved[i]));
198 }
199
200 if (self.stack_alloc != 0) {
201 try appendEncoding(out, allocator, encoding.subRegImm(.rsp, @intCast(self.stack_alloc)));
202 }
203 }
204
205 pub fn emitEpilogue(self: FrameLayout, allocator: std.mem.Allocator, out: *std.ArrayListUnmanaged(u8)) !void {
206 if (self.stack_alloc != 0) {
207 try appendEncoding(out, allocator, encoding.addRegImm(.rsp, @intCast(self.stack_alloc)));
208 }
209
210 var i: usize = self.num_callee_saved;
211 while (i > 0) : (i -= 1) {
212 try appendEncoding(out, allocator, encoding.popReg(self.used_callee_saved[i - 1]));
213 }
214
215 try appendEncoding(out, allocator, encoding.popReg(.rbp));
216 try appendEncoding(out, allocator, encoding.ret());
217 }
218 };
219
220 pub fn computeFrameLayout(
221 local_size: u32,
222 used_callee_saved: []const GPR,
223 ensure_call_alignment: bool,
224 ) FrameLayout {
225 var used: [callee_saved_gprs.len]GPR = callee_saved_gprs;
226 var count: usize = 0;
227 for (used_callee_saved) |reg| {
228 used[count] = reg;
229 count += 1;
230 }
231
232 var aligned_locals = local_size;
233 if (aligned_locals % stack_slot_size != 0) {
234 aligned_locals = aligned_locals + (stack_slot_size - (aligned_locals % stack_slot_size));
235 }
236
237 const push_bytes: u32 = stack_slot_size * @as(u32, @intCast(1 + count));
238 var stack_alloc: u32 = aligned_locals;
239
240 if (ensure_call_alignment) {
241 const desired: u32 = stack_alignment / 2;
242 const total = push_bytes + stack_alloc;
243 const rem = total % stack_alignment;
244 if (rem != desired) {
245 const delta = (stack_alignment + desired - rem) % stack_alignment;
246 stack_alloc += delta;
247 }
248 }
249
250 const padding = stack_alloc - aligned_locals;
251
252 return .{
253 .stack_alloc = stack_alloc,
254 .padding = padding,
255 .num_callee_saved = @intCast(count),
256 .used_callee_saved = used,
257 .call_aligned = ensure_call_alignment,
258 };
259 }
260
261 fn appendEncoding(out: *std.ArrayListUnmanaged(u8), allocator: std.mem.Allocator, enc: encoding.Encoding) !void {
262 try out.appendSlice(allocator, enc.slice());
263 }
264
265 test "SysV arg locations (integer + stack)" {
266 const types = [_][]const u8{
267 "arith.i64",
268 "arith.i64",
269 "arith.i64",
270 "arith.i64",
271 "arith.i64",
272 "arith.i64",
273 "arith.i64",
274 };
275 var locs: [types.len]ValueLocation = undefined;
276 computeArgLocations(&types, &locs);
277
278 try std.testing.expectEqual(int_arg_regs[0], locs[0].int_reg);
279 try std.testing.expectEqual(int_arg_regs[5], locs[5].int_reg);
280 try std.testing.expectEqual(@as(i32, 0), locs[6].stack);
281 }
282
283 test "SysV arg locations (float + stack)" {
284 const types = [_][]const u8{
285 "arith.f32",
286 "arith.f64",
287 "arith.f32",
288 "arith.f64",
289 "arith.f32",
290 "arith.f64",
291 "arith.f32",
292 "arith.f64",
293 "arith.f32",
294 };
295 var locs: [types.len]ValueLocation = undefined;
296 computeArgLocations(&types, &locs);
297
298 try std.testing.expectEqual(fp_arg_regs[0], locs[0].fp_reg);
299 try std.testing.expectEqual(fp_arg_regs[7], locs[7].fp_reg);
300 try std.testing.expectEqual(@as(i32, 0), locs[8].stack);
301 }
302
303 test "SysV return locations" {
304 try std.testing.expectEqual(int_return_reg, computeReturnLocation("arith.i64").int_reg);
305 try std.testing.expectEqual(fp_return_reg, computeReturnLocation("arith.f64").fp_reg);
306 }
307
308 test "SysV result pairs take the next register of each class" {
309 const mixed = [_][]const u8{ "arith.f64", "arith.bool" };
310 var locations: [max_register_results]ValueLocation = undefined;
311 computeReturnLocations(&mixed, &locations);
312 try std.testing.expectEqual(XMM.xmm0, locations[0].fp_reg);
313 try std.testing.expectEqual(GPR.rax, locations[1].int_reg);
314
315 const floats = [_][]const u8{ "arith.f32", "arith.f64" };
316 computeReturnLocations(&floats, &locations);
317 try std.testing.expectEqual(XMM.xmm1, locations[1].fp_reg);
318
319 const integers = [_][]const u8{ "arith.i32", "arith.i64" };
320 computeReturnLocations(&integers, &locations);
321 try std.testing.expectEqual(GPR.rdx, locations[1].int_reg);
322
323 try std.testing.expect(!returnsThroughRecord(max_register_results));
324 try std.testing.expect(returnsThroughRecord(max_register_results + 1));
325 try std.testing.expectEqual(@as(i32, 16), recordFieldOffset(2));
326 }
327
328 test "x86_64 signatures take no vector parameters and return a vector alone" {
329 const Signature = artifact.Signature;
330 const lanes = artifact.ValueType{ .vector = .{ .element = .f64, .lanes = 2 } };
331 const scalar = artifact.ValueType{ .scalar = .i64 };
332 const vector_result = try Signature.init(&.{ scalar, .memref }, &.{lanes});
333 try std.testing.expect(admitsSignature(&vector_result));
334 const product = try Signature.init(&.{scalar}, &.{ scalar, .memref, scalar });
335 try std.testing.expect(admitsSignature(&product));
336 const vector_parameter = try Signature.init(&.{lanes}, &.{scalar});
337 try std.testing.expect(!admitsSignature(&vector_parameter));
338 const vector_in_product = try Signature.init(&.{}, &.{ lanes, scalar });
339 try std.testing.expect(!admitsSignature(&vector_in_product));
340 }
341
342 test "SysV frame alignment padding" {
343 const layout = computeFrameLayout(0, &.{}, true);
344 const total = stack_slot_size + layout.stack_alloc;
345 try std.testing.expectEqual(@as(u32, 8), total % stack_alignment);
346 try std.testing.expectEqual(@as(u32, 0), layout.stack_alloc);
347 }
348
349 test "SysV call stack padding" {
350 const testing = std.testing;
351 try testing.expectEqual(@as(u32, 0), computeCallStackPadding(0, 0, true));
352 try testing.expectEqual(@as(u32, 8), computeCallStackPadding(8, 0, true));
353 try testing.expectEqual(@as(u32, 0), computeCallStackPadding(16, 0, true));
354 try testing.expectEqual(@as(u32, 0), computeCallStackPadding(8, 8, true));
355 try testing.expectEqual(@as(u32, 8), computeCallStackPadding(0, 8, false));
356 }
357
358 test "SysV prologue/epilogue encoding" {
359 const allocator = std.testing.allocator;
360 var buffer = std.ArrayListUnmanaged(u8).empty;
361 defer buffer.deinit(allocator);
362
363 const layout = computeFrameLayout(0, &.{}, true);
364 try layout.emitPrologue(allocator, &buffer);
365 try layout.emitEpilogue(allocator, &buffer);
366
367 try std.testing.expectEqualSlices(u8, &.{
368 0x55,
369 0x48,
370 0x89,
371 0xE5,
372 0x5D,
373 0xC3,
374 }, buffer.items);
375 }
376
377 test "x86_64 refuses signatures changed into invalid ones after init" {
378 const lanes4 = artifact.ValueType{ .vector = .{ .element = .f32, .lanes = 4 } };
379 var signature = try artifact.Signature.init(&.{.memref}, &.{lanes4});
380 try std.testing.expect(admitsSignature(&signature));
381 signature.result_types[0].vector.lanes = 0;
382 try std.testing.expect(!admitsSignature(&signature));
383 }