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 }