lib/choir/src/backends/x64/object.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_arena = @import("alloc_arena");
3 const alloc_observe = @import("alloc_observe");
4 const ir = @import("../../core/root.zig");
5 const diagnostics = @import("../../root.zig").diagnostics;
6 const artifact = @import("../root.zig").artifact;
7 const elf_object = @import("../root.zig").elf_object;
8 const interface = @import("../root.zig").interface;
9 const machine = @import("../root.zig").machine_code;
10 const boundary = @import("../root.zig").signature;
11 const datasym = @import("data.zig");
12 const emit = @import("emit.zig");
13 const failures = @import("failures.zig");
14 const sysv = @import("abi.zig");
15 const sys = @import("sys");
16
17 const BackendError = interface.BackendError;
18 const Allocator = std.mem.Allocator;
19
20 const supports_x86_64_backend = sys.capabilities.current.supportsX86_64Execution();
21
22 pub const ModuleCompileOptions = ir.ThreadingOptions;
23
24 pub fn target() artifact.Target {
25 return .{
26 .architecture = .x86_64,
27 .triple = "x86_64-unknown-linux-gnu",
28 .cpu = "x86-64",
29 };
30 }
31
32 pub fn abi() artifact.Abi {
33 return .{
34 .name = "sysv",
35 .calling_convention = "c",
36 .object_format = "elf",
37 .pointer_width_bits = 64,
38 .endianness = .little,
39 };
40 }
41
42 fn artifactRelocations(
43 allocator: Allocator,
44 call_relocations: []const machine.CallRelocation,
45 data_relocations: []const machine.DataRelocation,
46 ) BackendError![]artifact.Relocation {
47 const count = std.math.add(usize, call_relocations.len, data_relocations.len) catch {
48 return BackendError.OutOfMemory;
49 };
50 const relocations = allocator.alloc(artifact.Relocation, count) catch {
51 return BackendError.OutOfMemory;
52 };
53 for (call_relocations, 0..) |relocation, index| {
54 relocations[index] = .{
55 .offset = relocation.offset,
56 .symbol = relocation.target,
57 .kind = .call,
58 };
59 }
60 for (data_relocations, call_relocations.len..) |relocation, index| {
61 relocations[index] = .{
62 .offset = relocation.offset,
63 .symbol = relocation.target,
64 .kind = .absolute,
65 .addend = relocation.addend,
66 .width_bits = relocation.width_bits,
67 };
68 }
69 return relocations;
70 }
71
72 /// Emits `func` as machine code with its call relocations, data symbols, and data relocations,
73 /// which the caller frees with `deinit(allocator)`.
74 /// Fails with `error.CodeGenFailed` when emission fails, and reports one error diagnostic located
75 /// at the innermost operation whose emission failed, or otherwise at `func`.
76 /// Fails with `error.OutOfMemory` when an allocation fails, and reports nothing.
77 pub fn emitFunctionMachineCode(allocator: Allocator, func: *ir.Operation) BackendError!machine.MachineCode {
78 var failure: ?failures.Failure = null;
79 errdefer if (failure) |located| located.report();
80 return try emitFunctionMachineCodeWithFailure(allocator, func, &failure);
81 }
82
83 /// Emits `func` as machine code exactly as `emitFunctionMachineCode` does, and reports nothing.
84 /// Sets `failure` to the stage, the operation, and the emitter's own error when emission fails,
85 /// which is the specific name `BackendError` has no member for and which `error.CodeGenFailed`
86 /// stands in for. A caller states it in its own refusal, or reports it with `failure.report()`.
87 /// `failure` must be null on entry, and the operation it names lives as long as `func` does.
88 pub fn emitFunctionMachineCodeWithFailure(
89 allocator: Allocator,
90 func: *ir.Operation,
91 failure: *?failures.Failure,
92 ) BackendError!machine.MachineCode {
93 std.debug.assert(failure.* == null);
94 var emitter = emit.Emitter.init(allocator);
95 defer emitter.deinit();
96 emitter.emitFunction(func) catch |err| {
97 const operation = emitter.failed_operation orelse func;
98 failure.* = .{ .stage = .emit, .operation = operation, .err = err };
99 return switch (err) {
100 error.OutOfMemory => BackendError.OutOfMemory,
101 else => BackendError.CodeGenFailed,
102 };
103 };
104
105 const code = emitter.getCode();
106 std.debug.assert(code.len != 0);
107
108 const owned_code = allocator.dupe(u8, code) catch return BackendError.OutOfMemory;
109 errdefer allocator.free(owned_code);
110
111 const relocations = allocator.alloc(machine.CallRelocation, emitter.call_relocations.items.len) catch {
112 return BackendError.OutOfMemory;
113 };
114 errdefer allocator.free(relocations);
115
116 const data_symbols = allocator.dupe(machine.DataSymbol, emitter.data_symbols.items()) catch {
117 return BackendError.OutOfMemory;
118 };
119 errdefer allocator.free(data_symbols);
120
121 const data_relocations = allocator.alloc(machine.DataRelocation, emitter.data_relocations.items.len) catch {
122 return BackendError.OutOfMemory;
123 };
124 errdefer allocator.free(data_relocations);
125
126 for (emitter.call_relocations.items, 0..) |reloc, index| {
127 relocations[index] = .{ .offset = reloc.offset, .target = reloc.target };
128 }
129 for (emitter.data_relocations.items, 0..) |reloc, index| {
130 data_relocations[index] = reloc;
131 }
132
133 return .{
134 .code = owned_code,
135 .relocations = relocations,
136 .data_symbols = data_symbols,
137 .data_relocations = data_relocations,
138 };
139 }
140
141 /// Compiles one function into a machine-code artifact for the JIT loader.
142 /// The artifact records the function's signature. Each data symbol the function references
143 /// travels as a raw buffer beside the code.
144 /// Fails with `error.UnsupportedOperation` when this backend cannot record, pass, or return the
145 /// function's parameter and result types.
146 pub fn compileFunctionToArtifact(
147 allocator: Allocator,
148 func: *ir.Operation,
149 function_name: []const u8,
150 ) BackendError!artifact.Artifact {
151 const signature = boundary.ofFunction(func) catch return BackendError.UnsupportedOperation;
152 if (!sysv.admitsSignature(&signature)) return BackendError.UnsupportedOperation;
153 var machine_code = try emitFunctionMachineCode(allocator, func);
154 defer machine_code.deinit(allocator);
155
156 const data = allocator.alloc(artifact.DataBuffer, machine_code.data_symbols.len) catch {
157 return BackendError.OutOfMemory;
158 };
159 defer allocator.free(data);
160 for (machine_code.data_symbols, data) |symbol, *buffer| {
161 buffer.* = .{
162 .name = symbol.name,
163 .bytes = symbol.bytes,
164 .alignment = symbol.alignment,
165 .binding = switch (symbol.binding) {
166 .local => .local,
167 .global => .external,
168 },
169 };
170 }
171 const relocations = try artifactRelocations(
172 allocator,
173 machine_code.relocations,
174 machine_code.data_relocations,
175 );
176 defer allocator.free(relocations);
177
178 return artifact.machineCodeArtifact(
179 allocator,
180 target(),
181 abi(),
182 function_name,
183 signature,
184 machine_code.code,
185 data,
186 relocations,
187 ) catch BackendError.OutOfMemory;
188 }
189
190 pub fn compileFunctionToObjectFile(
191 allocator: Allocator,
192 func: *ir.Operation,
193 function_name: []const u8,
194 ) BackendError!artifact.Artifact {
195 var machine_code = try emitFunctionMachineCode(allocator, func);
196 defer machine_code.deinit(allocator);
197
198 const object = elf_object.buildX86_64MachineCodeObject(allocator, .{
199 .entry_symbol = function_name,
200 .code = machine_code.code,
201 .relocations = machine_code.relocations,
202 .data_relocations = machine_code.data_relocations,
203 .data_symbols = machine_code.data_symbols,
204 }) catch |err| switch (err) {
205 error.OutOfMemory => return BackendError.OutOfMemory,
206 else => return BackendError.CodeGenFailed,
207 };
208 defer allocator.free(object);
209
210 const relocations = try artifactRelocations(allocator, machine_code.relocations, machine_code.data_relocations);
211 defer allocator.free(relocations);
212
213 return artifact.objectFileArtifact(
214 allocator,
215 target(),
216 abi(),
217 function_name,
218 object,
219 relocations,
220 ) catch BackendError.OutOfMemory;
221 }
222
223 pub fn compileModuleToObjectFile(
224 allocator: Allocator,
225 lowered_module: *ir.Operation,
226 entry_name: []const u8,
227 options: ModuleCompileOptions,
228 ) BackendError!artifact.Artifact {
229 var functions = std.ArrayListUnmanaged(*ir.Operation).empty;
230 defer functions.deinit(allocator);
231 try appendFunctionDefinitions(allocator, lowered_module, &functions);
232 if (!hasFunctionDefinition(functions.items, entry_name)) return BackendError.FunctionNotFound;
233
234 var defined_functions = std.StringHashMapUnmanaged(void){};
235 defer defined_functions.deinit(allocator);
236 for (functions.items) |func| {
237 const name = functionSymbolName(func) orelse return BackendError.CodeGenFailed;
238 if (defined_functions.contains(name)) return BackendError.CodeGenFailed;
239 defined_functions.put(allocator, name, {}) catch return BackendError.OutOfMemory;
240 }
241
242 var text = std.ArrayListUnmanaged(u8).empty;
243 defer text.deinit(allocator);
244 var text_symbols = std.ArrayListUnmanaged(elf_object.TextSymbol).empty;
245 defer text_symbols.deinit(allocator);
246 var call_relocations = std.ArrayListUnmanaged(machine.CallRelocation).empty;
247 defer call_relocations.deinit(allocator);
248 var data_symbols: machine.DataSymbolSet = .{};
249 defer data_symbols.deinit(allocator);
250 var data_relocations = std.ArrayListUnmanaged(machine.DataRelocation).empty;
251 defer data_relocations.deinit(allocator);
252
253 datasym.appendModuleGlobals(allocator, &data_symbols, lowered_module) catch |err| switch (err) {
254 error.OutOfMemory => return BackendError.OutOfMemory,
255 else => {
256 failures.Failure.report(.{ .stage = .emit, .operation = lowered_module, .err = err });
257 return BackendError.CodeGenFailed;
258 },
259 };
260
261 const emitted_functions = try emitModuleFunctions(allocator, functions.items, options);
262 defer deinitEmittedFunctions(allocator, emitted_functions);
263
264 for (emitted_functions, functions.items) |emitted, func| {
265 const aligned_offset = alignForward(text.items.len, 16);
266 text.appendNTimes(allocator, 0, aligned_offset - text.items.len) catch {
267 return BackendError.OutOfMemory;
268 };
269
270 text.appendSlice(allocator, emitted.machine_code.code) catch return BackendError.OutOfMemory;
271 text_symbols.append(allocator, .{
272 .name = emitted.name,
273 .offset = @intCast(aligned_offset),
274 .size = @intCast(emitted.machine_code.code.len),
275 }) catch return BackendError.OutOfMemory;
276
277 for (emitted.machine_code.relocations) |relocation| {
278 call_relocations.append(allocator, .{
279 .offset = aligned_offset + relocation.offset,
280 .target = relocation.target,
281 }) catch return BackendError.OutOfMemory;
282 }
283 for (emitted.machine_code.data_symbols) |symbol| {
284 try putDataSymbol(allocator, &data_symbols, symbol, func);
285 }
286 for (emitted.machine_code.data_relocations) |relocation| {
287 data_relocations.append(allocator, .{
288 .offset = aligned_offset + relocation.offset,
289 .target = relocation.target,
290 .addend = relocation.addend,
291 .width_bits = relocation.width_bits,
292 }) catch return BackendError.OutOfMemory;
293 }
294 }
295
296 const object = elf_object.buildX86_64MachineCodeObject(allocator, .{
297 .entry_symbol = entry_name,
298 .code = text.items,
299 .text_symbols = text_symbols.items,
300 .relocations = call_relocations.items,
301 .data_relocations = data_relocations.items,
302 .data_symbols = data_symbols.items(),
303 }) catch |err| switch (err) {
304 error.OutOfMemory => return BackendError.OutOfMemory,
305 else => return BackendError.CodeGenFailed,
306 };
307 defer allocator.free(object);
308
309 const relocations = try artifactRelocations(allocator, call_relocations.items, data_relocations.items);
310 defer allocator.free(relocations);
311
312 var out = artifact.Artifact.init(allocator, .{
313 .kind = .object_file,
314 .target = target(),
315 .abi = abi(),
316 }) catch return BackendError.OutOfMemory;
317 errdefer out.deinit();
318
319 out.payload.addBuffer(.{
320 .name = entry_name,
321 .format = .object_file,
322 .bytes = object,
323 .alignment = 8,
324 }) catch return BackendError.OutOfMemory;
325 for (relocations) |relocation| {
326 out.linkage.addRelocation(relocation) catch return BackendError.OutOfMemory;
327 }
328 for (functions.items) |func| {
329 out.linkage.addProvided(.{
330 .name = functionSymbolName(func) orelse return BackendError.CodeGenFailed,
331 .kind = .function,
332 .binding = .external,
333 }) catch return BackendError.OutOfMemory;
334 }
335 for (call_relocations.items) |relocation| {
336 if (defined_functions.contains(relocation.target)) continue;
337 if (!out.linkage.hasRequired(relocation.target)) {
338 out.linkage.addRequired(.{
339 .name = relocation.target,
340 .kind = .function,
341 .binding = .external,
342 }) catch return BackendError.OutOfMemory;
343 }
344 }
345
346 return out;
347 }
348
349 fn appendFunctionDefinitions(
350 allocator: Allocator,
351 module: *ir.Operation,
352 definitions: *std.ArrayListUnmanaged(*ir.Operation),
353 ) BackendError!void {
354 const region = module.getRegion(0) orelse return BackendError.FunctionNotFound;
355 const block = region.getEntryBlock() orelse return BackendError.FunctionNotFound;
356
357 var op_iter = block.operations.head;
358 while (op_iter) |op_ptr| {
359 const op: *ir.Operation = @ptrCast(@alignCast(op_ptr));
360 op_iter = op.next_op;
361 if (!ir.inspection.isFunctionDefinition(op)) continue;
362 definitions.append(allocator, op) catch return BackendError.OutOfMemory;
363 }
364 }
365
366 fn hasFunctionDefinition(functions: []const *ir.Operation, name: []const u8) bool {
367 for (functions) |function| {
368 const actual = functionSymbolName(function) orelse continue;
369 if (std.mem.eql(u8, actual, name)) return true;
370 }
371 return false;
372 }
373
374 const EmittedFunction = struct {
375 name: []const u8,
376 machine_code: machine.MachineCode,
377
378 fn deinit(self: *EmittedFunction, allocator: Allocator) void {
379 self.machine_code.deinit(allocator);
380 self.* = undefined;
381 }
382 };
383
384 fn emitModuleFunctions(
385 allocator: Allocator,
386 functions: []const *ir.Operation,
387 options: ModuleCompileOptions,
388 ) BackendError![]EmittedFunction {
389 if (options.workerCount(functions.len) > 1) {
390 return try emitModuleFunctionsParallel(allocator, functions, options);
391 }
392 return try emitModuleFunctionsSerial(allocator, functions);
393 }
394
395 fn emitModuleFunctionsSerial(
396 allocator: Allocator,
397 functions: []const *ir.Operation,
398 ) BackendError![]EmittedFunction {
399 if (functions.len == 0) return &.{};
400
401 const emitted = allocator.alloc(EmittedFunction, functions.len) catch return BackendError.OutOfMemory;
402 var written: usize = 0;
403 errdefer {
404 deinitEmittedFunctionItems(allocator, emitted[0..written]);
405 allocator.free(emitted);
406 }
407
408 for (functions, 0..) |func, index| {
409 var failure: ?failures.Failure = null;
410 errdefer if (failure) |located| located.report();
411 emitted[index] = try emitOneModuleFunction(allocator, func, &failure);
412 written += 1;
413 }
414
415 return emitted;
416 }
417
418 fn emitModuleFunctionsParallel(
419 allocator: Allocator,
420 functions: []const *ir.Operation,
421 options: ModuleCompileOptions,
422 ) BackendError![]EmittedFunction {
423 if (functions.len == 0) return &.{};
424
425 const worker_allocator = options.workerAllocator(allocator);
426 const slots = allocator.alloc(EmitWorkerSlot, functions.len) catch return BackendError.OutOfMemory;
427 defer allocator.free(slots);
428 for (slots) |*slot| slot.* = .{};
429 defer deinitEmitWorkerSlots(worker_allocator, slots);
430
431 var batch = EmitWorkerBatch{
432 .allocator = worker_allocator,
433 .functions = functions,
434 .slots = slots,
435 };
436
437 ir.threading.parallelForEachIndex(allocator, options, functions.len, &batch, runEmitWorker) catch |err| switch (err) {
438 error.OutOfMemory => return BackendError.OutOfMemory,
439 else => return BackendError.CodeGenFailed,
440 };
441
442 if (firstFailedEmitWorker(slots)) |slot| {
443 if (slot.failure) |located| located.report();
444 return slot.result.err;
445 }
446
447 const emitted = allocator.alloc(EmittedFunction, functions.len) catch return BackendError.OutOfMemory;
448 var written: usize = 0;
449 errdefer {
450 deinitEmittedFunctionItems(allocator, emitted[0..written]);
451 allocator.free(emitted);
452 }
453
454 const transfer_results = sameAllocator(allocator, worker_allocator);
455 for (slots, 0..) |*slot, index| {
456 const worker_emitted = switch (slot.result) {
457 .ok => |result| result,
458 .pending => unreachable,
459 .err => unreachable,
460 };
461 emitted[index] = if (transfer_results)
462 worker_emitted
463 else
464 try cloneEmittedFunction(allocator, worker_emitted);
465 written += 1;
466 if (transfer_results) slot.result = .pending;
467 }
468
469 return emitted;
470 }
471
472 fn cloneEmittedFunction(allocator: Allocator, emitted: EmittedFunction) BackendError!EmittedFunction {
473 return .{
474 .name = emitted.name,
475 .machine_code = try cloneMachineCode(allocator, emitted.machine_code),
476 };
477 }
478
479 fn cloneMachineCode(allocator: Allocator, code: machine.MachineCode) BackendError!machine.MachineCode {
480 var cloned = machine.MachineCode{
481 .code = allocator.dupe(u8, code.code) catch return BackendError.OutOfMemory,
482 .relocations = &.{},
483 .data_symbols = &.{},
484 .data_relocations = &.{},
485 };
486 errdefer cloned.deinit(allocator);
487
488 cloned.relocations = allocator.dupe(machine.CallRelocation, code.relocations) catch return BackendError.OutOfMemory;
489 cloned.data_symbols = allocator.dupe(machine.DataSymbol, code.data_symbols) catch return BackendError.OutOfMemory;
490 cloned.data_relocations = allocator.dupe(machine.DataRelocation, code.data_relocations) catch return BackendError.OutOfMemory;
491
492 return cloned;
493 }
494
495 fn emitOneModuleFunction(
496 allocator: Allocator,
497 func: *ir.Operation,
498 failure: *?failures.Failure,
499 ) BackendError!EmittedFunction {
500 return .{
501 .name = functionSymbolName(func) orelse return BackendError.CodeGenFailed,
502 .machine_code = try emitFunctionMachineCodeWithFailure(allocator, func, failure),
503 };
504 }
505
506 const EmitWorkerResult = union(enum) {
507 pending,
508 ok: EmittedFunction,
509 err: BackendError,
510 };
511
512 const EmitWorkerSlot = struct {
513 result: EmitWorkerResult = .pending,
514 failure: ?failures.Failure = null,
515 };
516
517 const EmitWorkerBatch = struct {
518 allocator: Allocator,
519 functions: []const *ir.Operation,
520 slots: []EmitWorkerSlot,
521 };
522
523 fn runEmitWorker(batch: *EmitWorkerBatch, index: usize) void {
524 const slot = &batch.slots[index];
525 const func = batch.functions[index];
526 slot.result = if (emitOneModuleFunction(batch.allocator, func, &slot.failure)) |emitted|
527 .{ .ok = emitted }
528 else |err|
529 .{ .err = err };
530 }
531
532 fn firstFailedEmitWorker(slots: []const EmitWorkerSlot) ?*const EmitWorkerSlot {
533 for (slots) |*slot| {
534 if (slot.result == .err) return slot;
535 }
536 return null;
537 }
538
539 fn deinitEmittedFunctions(allocator: Allocator, emitted: []EmittedFunction) void {
540 deinitEmittedFunctionItems(allocator, emitted);
541 if (emitted.len != 0) allocator.free(emitted);
542 }
543
544 fn deinitEmittedFunctionItems(allocator: Allocator, emitted: []EmittedFunction) void {
545 for (emitted) |*function| function.deinit(allocator);
546 }
547
548 fn deinitEmitWorkerSlots(allocator: Allocator, slots: []EmitWorkerSlot) void {
549 for (slots) |*slot| {
550 switch (slot.result) {
551 .ok => |*function| function.deinit(allocator),
552 else => {},
553 }
554 }
555 }
556
557 fn sameAllocator(left: Allocator, right: Allocator) bool {
558 return left.ptr == right.ptr and left.vtable == right.vtable;
559 }
560
561 fn functionSymbolName(op: *ir.Operation) ?[]const u8 {
562 if (!std.mem.eql(u8, op.name.name, "func.func")) return null;
563 return ir.SymbolTable.getSymbolName(op);
564 }
565
566 fn putDataSymbol(
567 allocator: Allocator,
568 symbols: *machine.DataSymbolSet,
569 symbol: machine.DataSymbol,
570 func: *ir.Operation,
571 ) BackendError!void {
572 symbols.put(allocator, symbol) catch |err| switch (err) {
573 error.OutOfMemory => return BackendError.OutOfMemory,
574 error.InvalidDataSymbol,
575 error.ConflictingDataSymbol,
576 => {
577 failures.Failure.report(.{ .stage = .emit, .operation = func, .err = err });
578 return BackendError.CodeGenFailed;
579 },
580 };
581 }
582
583 fn alignForward(value: usize, alignment: usize) usize {
584 const mask = alignment - 1;
585 return (value + mask) & ~mask;
586 }
587
588 test "x86_64 machine relocations become generic artifact relocations" {
589 const testing = std.testing;
590 const relocations = try artifactRelocations(
591 testing.allocator,
592 &.{.{ .offset = 8, .target = "runtime_call" }},
593 &.{.{ .offset = 24, .target = "literal", .addend = -4, .width_bits = 32 }},
594 );
595 defer testing.allocator.free(relocations);
596
597 try testing.expectEqual(@as(usize, 2), relocations.len);
598 try testing.expectEqual(@as(u64, 8), relocations[0].offset);
599 try testing.expectEqualStrings("runtime_call", relocations[0].symbol);
600 try testing.expectEqual(artifact.RelocationKind.call, relocations[0].kind);
601 try testing.expectEqual(@as(u64, 24), relocations[1].offset);
602 try testing.expectEqualStrings("literal", relocations[1].symbol);
603 try testing.expectEqual(artifact.RelocationKind.absolute, relocations[1].kind);
604 try testing.expectEqual(@as(i64, -4), relocations[1].addend);
605 try testing.expectEqual(@as(?u16, 32), relocations[1].width_bits);
606 }
607
608 test "x86_64 object emits relocatable function artifact" {
609 if (!supports_x86_64_backend) return;
610
611 const testing = std.testing;
612 const dialects = @import("../../dialects/root.zig");
613 const ArithDialect = dialects.ArithDialect;
614 const BuiltinDialect = dialects.BuiltinDialect;
615 const FuncDialect = dialects.FuncDialect;
616
617 var arena = alloc_arena.Arena.init(std.testing.allocator);
618 defer arena.deinit();
619 const allocator = arena.allocator();
620
621 var ir_ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
622 defer ir_ctx.deinit(allocator);
623 try @import("../../dialects/root.zig").registerAllDialects(&ir_ctx);
624
625 const loc = ir.Location.getUnknown();
626 const i64_type = try ArithDialect.getScalarType(&ir_ctx, .i64);
627
628 const module = try BuiltinDialect.ModuleOp.create(&ir_ctx, loc);
629 const module_block = module.getBodyBlock();
630
631 const decl_name = "choir_handle_add1";
632 const declaration = try FuncDialect.FuncOp.createDeclaration(
633 &ir_ctx,
634 loc,
635 decl_name,
636 &.{i64_type},
637 &.{i64_type},
638 );
639 try module_block.addOperation(declaration.op);
640
641 var caller = try FuncDialect.FuncOp.create(&ir_ctx, loc, "call_handle_decl", &.{}, &.{i64_type});
642 try module_block.addOperation(caller.op);
643
644 const entry = caller.getEntryBlock();
645 var forty_one = try ArithDialect.ConstantOp.createInt(&ir_ctx, loc, i64_type, 41);
646 try entry.addOperation(forty_one.op);
647
648 var call = try FuncDialect.CallOp.create(&ir_ctx, loc, decl_name, &.{forty_one.getResult()}, &.{i64_type});
649 try entry.addOperation(call.op);
650
651 const ret = try FuncDialect.ReturnOp.create(&ir_ctx, loc, &.{call.getResult(0).?});
652 try entry.addOperation(ret.op);
653
654 var object_artifact = try compileFunctionToObjectFile(allocator, caller.op, "call_handle_decl");
655 defer object_artifact.deinit();
656
657 try testing.expectEqual(artifact.ArtifactKind.object_file, object_artifact.metadata.kind);
658 try testing.expectEqual(artifact.BufferFormat.object_file, object_artifact.payload.buffers.items[0].format);
659 try testing.expectEqualSlices(u8, std.elf.MAGIC, object_artifact.payload.buffers.items[0].bytes[0..4]);
660 try testing.expect(object_artifact.linkage.hasRequired(decl_name));
661 try testing.expectEqual(@as(usize, 1), object_artifact.linkage.relocations.items.len);
662 try testing.expectEqualStrings(decl_name, object_artifact.linkage.relocations.items[0].symbol);
663 }
664
665 test "x86_64 object threaded module matches serial object" {
666 if (!supports_x86_64_backend) return;
667
668 const testing = std.testing;
669 const dialects = @import("../../dialects/root.zig");
670 const ArithDialect = dialects.ArithDialect;
671 const BuiltinDialect = dialects.BuiltinDialect;
672 const FuncDialect = dialects.FuncDialect;
673
674 var arena = alloc_arena.Arena.init(std.testing.allocator);
675 defer arena.deinit();
676 const allocator = arena.allocator();
677
678 var ir_ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
679 defer ir_ctx.deinit(allocator);
680 try @import("../../dialects/root.zig").registerAllDialects(&ir_ctx);
681
682 const loc = ir.Location.getUnknown();
683 const i64_type = try ArithDialect.getScalarType(&ir_ctx, .i64);
684
685 const module = try BuiltinDialect.ModuleOp.create(&ir_ctx, loc);
686 const module_block = module.getBodyBlock();
687
688 var entry = try FuncDialect.FuncOp.create(&ir_ctx, loc, "entry", &.{}, &.{i64_type});
689 try module_block.addOperation(entry.op);
690 var entry_value = try ArithDialect.ConstantOp.createInt(&ir_ctx, loc, i64_type, 7);
691 try entry.getEntryBlock().addOperation(entry_value.op);
692 const entry_ret = try FuncDialect.ReturnOp.create(&ir_ctx, loc, &.{entry_value.getResult()});
693 try entry.getEntryBlock().addOperation(entry_ret.op);
694
695 var helper = try FuncDialect.FuncOp.create(&ir_ctx, loc, "helper", &.{}, &.{i64_type});
696 try module_block.addOperation(helper.op);
697 var helper_value = try ArithDialect.ConstantOp.createInt(&ir_ctx, loc, i64_type, 11);
698 try helper.getEntryBlock().addOperation(helper_value.op);
699 const helper_ret = try FuncDialect.ReturnOp.create(&ir_ctx, loc, &.{helper_value.getResult()});
700 try helper.getEntryBlock().addOperation(helper_ret.op);
701
702 var serial = try compileModuleToObjectFile(allocator, module.op, "entry", .{});
703 defer serial.deinit();
704
705 var threaded = threaded: {
706 var worker_gpa = alloc_observe.debug.Allocator(.{}).init(
707 testing.allocator,
708 );
709 defer {
710 const status = worker_gpa.deinit();
711 testing.expect(status == .ok) catch @panic("x86_64 worker emission leaked allocations");
712 }
713 break :threaded try compileModuleToObjectFile(allocator, module.op, "entry", .{
714 .max_threads = 2,
715 .worker_allocator = worker_gpa.allocator(),
716 });
717 };
718 defer threaded.deinit();
719
720 try testing.expectEqual(artifact.ArtifactKind.object_file, threaded.metadata.kind);
721 try testing.expectEqual(@as(usize, 2), threaded.linkage.provided_symbols.items.len);
722 try testing.expect(threaded.linkage.hasProvided("entry"));
723 try testing.expect(threaded.linkage.hasProvided("helper"));
724 try testing.expectEqualSlices(u8, serial.payload.buffers.items[0].bytes, threaded.payload.buffers.items[0].bytes);
725 }
726
727 test "x86_64 object emission reports each failure once on the calling thread" {
728 if (!supports_x86_64_backend) return;
729
730 const testing = std.testing;
731 const dialects = @import("../../dialects/root.zig");
732 const ArithDialect = dialects.ArithDialect;
733 const FuncDialect = dialects.FuncDialect;
734
735 var arena = alloc_arena.Arena.init(testing.allocator);
736 defer arena.deinit();
737 const allocator = arena.allocator();
738 var ir_ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
739 defer ir_ctx.deinit(allocator);
740 try dialects.registerAllDialects(&ir_ctx);
741
742 const loc = ir.Location.getFile("object-failure.choir", 2, 7);
743 const u64_type = try ArithDialect.getScalarType(&ir_ctx, .u64);
744 const module = try dialects.BuiltinDialect.ModuleOp.create(&ir_ctx, loc);
745 var shift: ?*ir.Operation = null;
746 for ([_][]const u8{ "entry", "helper" }) |name| {
747 const function = try FuncDialect.FuncOp.create(&ir_ctx, loc, name, &.{}, &.{u64_type});
748 try module.getBodyBlock().addOperation(function.op);
749 const entry = function.getEntryBlock();
750 const value = try ArithDialect.ConstantOp.createInt(&ir_ctx, loc, u64_type, 16);
751 try entry.addOperation(value.op);
752 var result = value.getResult();
753 if (std.mem.eql(u8, name, "helper")) {
754 const shr = try ArithDialect.ShrOp.create(&ir_ctx, loc, result, result);
755 try entry.addOperation(shr.op);
756 shift = shr.op;
757 result = shr.getResult();
758 }
759 const ret = try FuncDialect.ReturnOp.create(&ir_ctx, loc, &.{result});
760 try entry.addOperation(ret.op);
761 }
762
763 var worker_gpa = alloc_observe.debug.Allocator(.{}).init(testing.allocator);
764 defer {
765 const status = worker_gpa.deinit();
766 testing.expect(status == .ok) catch @panic("x86_64 worker emission leaked allocations");
767 }
768 var reports = diagnostics.CaptureBuffer.init(testing.allocator);
769 defer reports.deinit();
770 var capture = ir_ctx.captureDiagnostics(&reports);
771 var guard = capture.enter();
772 defer guard.deinit();
773
774 const serial: ModuleCompileOptions = .{};
775 const threaded: ModuleCompileOptions = .{
776 .max_threads = 2,
777 .worker_allocator = worker_gpa.allocator(),
778 };
779 for ([_]ModuleCompileOptions{ serial, threaded }, 1..) |options, reported| {
780 const compiled = compileModuleToObjectFile(allocator, module.op, "entry", options);
781 try testing.expectError(BackendError.CodeGenFailed, compiled);
782 try testing.expectEqual(reported, reports.diagnostics.items.len);
783 const rejected = reports.diagnostics.items[reported - 1];
784 try testing.expectEqual(shift.?, rejected.operation.?);
785 try testing.expectEqualStrings(failures.Stage.emit.name(), rejected.metadata[0].value);
786 }
787 }
788
789 test "x86_64 object assembly reports conflicting data symbols at their function" {
790 if (!supports_x86_64_backend) return;
791
792 const testing = std.testing;
793 const dialects = @import("../../dialects/root.zig");
794 const ArithDialect = dialects.ArithDialect;
795 const FuncDialect = dialects.FuncDialect;
796
797 var arena = alloc_arena.Arena.init(testing.allocator);
798 defer arena.deinit();
799 const allocator = arena.allocator();
800 var ir_ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
801 defer ir_ctx.deinit(allocator);
802 try dialects.registerAllDialects(&ir_ctx);
803
804 const loc = ir.Location.getFile("object-data.choir", 4, 1);
805 const i64_type = try ArithDialect.getScalarType(&ir_ctx, .i64);
806 const module = try dialects.BuiltinDialect.ModuleOp.create(&ir_ctx, loc);
807 const names = machine.data_symbol_attr_names;
808 const tables = [_][]const u8{ "one", "two" };
809 var functions: [tables.len]*ir.Operation = undefined;
810 for (&functions, tables, 0..) |*function_op, bytes, index| {
811 const name = if (index == 0) "entry" else "other";
812 const function = try FuncDialect.FuncOp.create(&ir_ctx, loc, name, &.{}, &.{i64_type});
813 try module.getBodyBlock().addOperation(function.op);
814 const address = try ArithDialect.ConstantOp.createInt(&ir_ctx, loc, i64_type, 7);
815 try function.getEntryBlock().addOperation(address.op);
816 try address.op.setAttr(names.name, try ir_ctx.getStringAttr("table"));
817 try address.op.setAttr(names.bytes, try ir_ctx.getStringAttr(bytes));
818 const ret = try FuncDialect.ReturnOp.create(&ir_ctx, loc, &.{address.getResult()});
819 try function.getEntryBlock().addOperation(ret.op);
820 function_op.* = function.op;
821 }
822
823 var reports = diagnostics.CaptureBuffer.init(testing.allocator);
824 defer reports.deinit();
825 var capture = ir_ctx.captureDiagnostics(&reports);
826 var guard = capture.enter();
827 defer guard.deinit();
828 const compiled = compileModuleToObjectFile(allocator, module.op, "entry", .{});
829 try testing.expectError(BackendError.CodeGenFailed, compiled);
830 try testing.expectEqual(@as(usize, 1), reports.diagnostics.items.len);
831 const conflict = reports.diagnostics.items[0];
832 try testing.expectEqual(functions[1], conflict.operation.?);
833 try testing.expectEqualStrings("ConflictingDataSymbol", conflict.metadata[1].value);
834 }
835
836 /// Reads a relocatable object back through `std.elf`.
837 ///
838 /// The package's own reader is a set of private helpers inside the writer's own file, so a pin
839 /// that used them could agree with a writer that had the layout wrong. `std.elf` shares no code
840 /// with `backends/elf.zig` and its typed section and symbol records are the independent account.
841 const ObjectView = struct {
842 bytes: []const u8,
843
844 const Ehdr = std.elf.Elf64.Ehdr;
845 const Shdr = std.elf.Elf64.Shdr;
846 const Sym = std.elf.Elf64.Sym;
847 const Rela = std.elf.Elf64.Rela;
848
849 fn value(self: ObjectView, comptime T: type, offset: usize) T {
850 return std.mem.bytesToValue(T, self.bytes[offset..][0..@sizeOf(T)]);
851 }
852
853 fn record(self: ObjectView, comptime T: type, section: Shdr, index: usize) T {
854 return self.value(T, @as(usize, @intCast(section.offset)) + index * @sizeOf(T));
855 }
856
857 fn count(comptime T: type, section: Shdr) usize {
858 return @as(usize, @intCast(section.size)) / @sizeOf(T);
859 }
860
861 fn sectionAt(self: ObjectView, index: usize) Shdr {
862 const head = self.value(Ehdr, 0);
863 return self.value(Shdr, @as(usize, @intCast(head.shoff)) + index * @sizeOf(Shdr));
864 }
865
866 fn stringAt(self: ObjectView, table: Shdr, offset: u32) []const u8 {
867 const start = @as(usize, @intCast(table.offset)) + offset;
868 const end = std.mem.indexOfScalarPos(u8, self.bytes, start, 0) orelse self.bytes.len;
869 return self.bytes[start..end];
870 }
871
872 fn sectionIndex(self: ObjectView, name: []const u8) ?u16 {
873 const head = self.value(Ehdr, 0);
874 const names = self.sectionAt(head.shstrndx);
875 for (0..head.shnum) |index| {
876 const section = self.sectionAt(index);
877 if (!std.mem.eql(u8, self.stringAt(names, section.name), name)) continue;
878 return @intCast(index);
879 }
880 return null;
881 }
882
883 fn symbol(self: ObjectView, name: []const u8) ?Sym {
884 const symtab = self.sectionAt(self.sectionIndex(".symtab") orelse return null);
885 const strtab = self.sectionAt(symtab.link);
886 for (0..count(Sym, symtab)) |index| {
887 const entry = self.record(Sym, symtab, index);
888 if (!std.mem.eql(u8, self.stringAt(strtab, entry.name), name)) continue;
889 return entry;
890 }
891 return null;
892 }
893
894 /// How many `.rela.text` entries name `symbol_name`, which is one per reference from code.
895 fn textRelocations(self: ObjectView, symbol_name: []const u8) usize {
896 const rela = self.sectionAt(self.sectionIndex(".rela.text") orelse return 0);
897 const symtab = self.sectionAt(self.sectionIndex(".symtab") orelse return 0);
898 const strtab = self.sectionAt(symtab.link);
899 var matches: usize = 0;
900 for (0..count(Rela, rela)) |index| {
901 const entry = self.record(Rela, rela, index);
902 const named = self.record(Sym, symtab, entry.info.sym);
903 if (!std.mem.eql(u8, self.stringAt(strtab, named.name), symbol_name)) continue;
904 matches += 1;
905 }
906 return matches;
907 }
908 };
909
910 /// Builds a module that declares a bump region and one function that names two of its globals.
911 ///
912 /// `arena.limit` is declared and named by nothing, which is how the pin tells a symbol the object
913 /// carries because the module declares it from one a use site happened to register.
914 fn buildRegionProbeModule(ir_ctx: *ir.Context) !*ir.Operation {
915 const dialects = @import("../../dialects/root.zig");
916 const ArithDialect = dialects.ArithDialect;
917 const BuiltinDialect = dialects.BuiltinDialect;
918 const FuncDialect = dialects.FuncDialect;
919 const MemrefDialect = dialects.MemrefDialect;
920
921 const loc = ir.Location.getUnknown();
922 const i64_type = try ArithDialect.getScalarType(ir_ctx, .i64);
923 const u8_type = try ArithDialect.getScalarType(ir_ctx, .u8);
924 const index_type = try ArithDialect.getIndexType(ir_ctx);
925 const arena_type = try MemrefDialect.getMemrefType1D(ir_ctx, 4096, u8_type, .host);
926 const word_type = try MemrefDialect.getMemrefType1D(ir_ctx, 1, i64_type, .host);
927
928 const module = try BuiltinDialect.ModuleOp.create(ir_ctx, loc);
929 const module_block = module.getBodyBlock();
930 const declarations = .{
931 .{ "arena", arena_type, 16 },
932 .{ "arena.cursor", word_type, 8 },
933 .{ "arena.limit", word_type, 8 },
934 };
935 inline for (declarations) |declaration| {
936 const global = try MemrefDialect.GlobalOp.create(ir_ctx, loc, .{
937 .sym_name = declaration[0],
938 .memref_type = declaration[1],
939 .alignment = declaration[2],
940 });
941 try module_block.addOperation(global.op);
942 }
943
944 var probe = try FuncDialect.FuncOp.create(ir_ctx, loc, "probe", &.{i64_type}, &.{i64_type});
945 try module_block.addOperation(probe.op);
946 const entry = probe.getEntryBlock();
947 var zero = try ArithDialect.ConstantOp.createInt(ir_ctx, loc, index_type, 0);
948 try entry.addOperation(zero.op);
949 const cursor = try MemrefDialect.GetGlobalOp.create(ir_ctx, loc, "arena.cursor", word_type);
950 try entry.addOperation(cursor.op);
951 var used = try MemrefDialect.LoadOp.create(
952 ir_ctx,
953 loc,
954 cursor.getResult(),
955 zero.getResult(),
956 i64_type,
957 );
958 try entry.addOperation(used.op);
959 const first_base = try MemrefDialect.GetGlobalOp.create(ir_ctx, loc, "arena", arena_type);
960 try entry.addOperation(first_base.op);
961 var first = try MemrefDialect.LoadOp.create(
962 ir_ctx,
963 loc,
964 first_base.getResult(),
965 used.getResult(),
966 i64_type,
967 );
968 try entry.addOperation(first.op);
969 const second_base = try MemrefDialect.GetGlobalOp.create(ir_ctx, loc, "arena", arena_type);
970 try entry.addOperation(second_base.op);
971 var second = try MemrefDialect.LoadOp.create(
972 ir_ctx,
973 loc,
974 second_base.getResult(),
975 probe.getArgument(0),
976 i64_type,
977 );
978 try entry.addOperation(second.op);
979 const sum = try ArithDialect.AddOp.create(ir_ctx, loc, first.getResult(), second.getResult());
980 try entry.addOperation(sum.op);
981 const ret = try FuncDialect.ReturnOp.create(ir_ctx, loc, &.{sum.getResult()});
982 try entry.addOperation(ret.op);
983 return module.op;
984 }
985
986 test "x86_64 module objects declare region globals as zero filled symbols" {
987 if (!supports_x86_64_backend) return;
988
989 const testing = std.testing;
990 var arena = alloc_arena.Arena.init(std.testing.allocator);
991 defer arena.deinit();
992 const allocator = arena.allocator();
993
994 var ir_ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
995 defer ir_ctx.deinit(allocator);
996 try @import("../../dialects/root.zig").registerAllDialects(&ir_ctx);
997
998 const module = try buildRegionProbeModule(&ir_ctx);
999 var object_artifact = try compileModuleToObjectFile(allocator, module, "probe", .{});
1000 defer object_artifact.deinit();
1001
1002 const view = ObjectView{ .bytes = object_artifact.payload.buffers.items[0].bytes };
1003 const bss_index = view.sectionIndex(".bss") orelse return error.TestUnexpectedResult;
1004 const bss = view.sectionAt(bss_index);
1005 try testing.expectEqual(std.elf.SHT.NOBITS, bss.type);
1006 try testing.expectEqual(@as(u64, 16), bss.addralign);
1007
1008 const backing = view.symbol("arena") orelse return error.TestUnexpectedResult;
1009 try testing.expectEqual(bss_index, backing.shndx);
1010 try testing.expectEqual(@as(u64, 4096), backing.size);
1011 try testing.expectEqual(std.elf.STB.GLOBAL, backing.info.bind);
1012 try testing.expectEqual(std.elf.STT.OBJECT, backing.info.type);
1013 try testing.expectEqual(@as(u64, 0), backing.value % 16);
1014
1015 const cursor = view.symbol("arena.cursor") orelse return error.TestUnexpectedResult;
1016 try testing.expectEqual(bss_index, cursor.shndx);
1017 try testing.expectEqual(@as(u64, 8), cursor.size);
1018 try testing.expectEqual(@as(u64, 0), cursor.value % 8);
1019
1020 const limit = view.symbol("arena.limit") orelse return error.TestUnexpectedResult;
1021 try testing.expectEqual(bss_index, limit.shndx);
1022 try testing.expectEqual(@as(u64, 8), limit.size);
1023
1024 try testing.expectEqual(@as(usize, 2), view.textRelocations("arena"));
1025 try testing.expectEqual(@as(usize, 1), view.textRelocations("arena.cursor"));
1026 try testing.expectEqual(@as(usize, 0), view.textRelocations("arena.limit"));
1027 }
1028
1029 /// A function the emitter refuses, and the operation inside it that it refuses.
1030 const OutsizedWhile = struct {
1031 function: *ir.Operation,
1032 loop: *ir.Operation,
1033 };
1034
1035 /// Builds a function whose body is one `scf.while` carrying one value more than
1036 /// `emit.max_while_carried_values`. It is the one emitter failure whose name states a bound that
1037 /// was met rather than a module that disagrees with itself, so it is what a consumer most needs
1038 /// to read off a failure.
1039 fn buildOutsizedWhile(ctx: *ir.Context, allocator: Allocator) !OutsizedWhile {
1040 const dialects = @import("../../dialects/root.zig");
1041 const loc = ir.Location.getFile("object-reason.choir", 3, 1);
1042 const i64_type = try dialects.ArithDialect.getScalarType(ctx, .i64);
1043 const function = try dialects.FuncDialect.FuncOp.create(ctx, loc, "carry", &.{i64_type}, &.{i64_type});
1044 const entry = function.getEntryBlock();
1045
1046 var inits: std.ArrayListUnmanaged(*ir.Value) = .empty;
1047 defer inits.deinit(allocator);
1048 var result_types: std.ArrayListUnmanaged(ir.Type) = .empty;
1049 defer result_types.deinit(allocator);
1050 for (0..emit.max_while_carried_values + 1) |_| {
1051 try inits.append(allocator, function.getArgument(0));
1052 try result_types.append(allocator, i64_type);
1053 }
1054
1055 var loop = try dialects.ScfDialect.WhileOp.create(ctx, loc, inits.items, result_types.items);
1056 try entry.addOperation(loop.op);
1057 const before = loop.getBeforeBlock();
1058 const after = loop.getAfterBlock();
1059 const condition = try dialects.ScfDialect.ConditionOp.create(
1060 ctx,
1061 loc,
1062 before.arguments.items[0],
1063 before.arguments.items,
1064 );
1065 try before.addOperation(condition.op);
1066 const yield = try dialects.ScfDialect.YieldOp.create(ctx, loc, after.arguments.items);
1067 try after.addOperation(yield.op);
1068 const ret = try dialects.FuncDialect.ReturnOp.create(ctx, loc, &.{loop.op.getResult(0).?});
1069 try entry.addOperation(ret.op);
1070
1071 return .{ .function = function.op, .loop = loop.op };
1072 }
1073
1074 test "x86_64 a caller that asks for the failure reads the emitter's own error and no report" {
1075 if (!supports_x86_64_backend) return;
1076
1077 const testing = std.testing;
1078 const dialects = @import("../../dialects/root.zig");
1079
1080 var arena = alloc_arena.Arena.init(testing.allocator);
1081 defer arena.deinit();
1082 const allocator = arena.allocator();
1083 var ir_ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1084 defer ir_ctx.deinit(allocator);
1085 try dialects.registerAllDialects(&ir_ctx);
1086
1087 const outsized = try buildOutsizedWhile(&ir_ctx, allocator);
1088
1089 var reports = diagnostics.CaptureBuffer.init(testing.allocator);
1090 defer reports.deinit();
1091 var capture = ir_ctx.captureDiagnostics(&reports);
1092 var guard = capture.enter();
1093 defer guard.deinit();
1094
1095 var failure: ?failures.Failure = null;
1096 try testing.expectError(
1097 BackendError.CodeGenFailed,
1098 emitFunctionMachineCodeWithFailure(allocator, outsized.function, &failure),
1099 );
1100 try testing.expect(failure != null);
1101 try testing.expectEqual(failures.Stage.emit, failure.?.stage);
1102 try testing.expectEqual(@as(anyerror, error.TooManyWhileValues), failure.?.err);
1103 try testing.expectEqual(outsized.loop, failure.?.operation);
1104 try testing.expectEqual(@as(usize, 0), reports.diagnostics.items.len);
1105
1106 try testing.expectError(
1107 BackendError.CodeGenFailed,
1108 emitFunctionMachineCode(allocator, outsized.function),
1109 );
1110 try testing.expectEqual(@as(usize, 1), reports.diagnostics.items.len);
1111 const reported = reports.diagnostics.items[0];
1112 try testing.expectEqual(outsized.loop, reported.operation.?);
1113 try testing.expectEqualStrings(@errorName(failure.?.err), reported.metadata[1].value);
1114 }