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 }