Commit 3743c3e39c

mlugg <mlugg@mlugg.co.uk>
2025-05-28 07:36:47
compiler: slightly untangle LLVM from the linkers
The main goal of this commit is to make it easier to decouple codegen from the linkers by being able to do LLVM codegen without going through the `link.File`; however, this ended up being a nice refactor anyway. Previously, every linker stored an optional `llvm.Object`, which was populated when using LLVM for the ZCU *and* linking an output binary; and `Zcu` also stored an optional `llvm.Object`, which was used only when we needed LLVM for the ZCU (e.g. for `-femit-llvm-bc`) but were not emitting a binary. This situation was incredibly silly. It meant there were N+1 places the LLVM object might be instead of just 1, and it meant that every linker had to start a bunch of methods by checking for an LLVM object, and just dispatching to the corresponding method on *it* instead if it was not `null`. Instead, we now always store the LLVM object on the `Zcu` -- which makes sense, because it corresponds to the object emitted by, well, the Zig Compilation Unit! The linkers now mostly don't make reference to LLVM. `Compilation` makes sure to emit the LLVM object if necessary before calling `flush`, so it is ready for the linker. Also, all of the `link.File` methods which act on the ZCU -- like `updateNav` -- now check for the LLVM object in `link.zig` instead of in every single individual linker implementation. Notably, the change to LLVM emit improves this rather ludicrous call chain in the `-fllvm -flld` case: * Compilation.flush * link.File.flush * link.Elf.flush * link.Elf.linkWithLLD * link.Elf.flushModule * link.emitLlvmObject * Compilation.emitLlvmObject * llvm.Object.emit Replacing it with this one: * Compilation.flush * llvm.Object.emit ...although we do currently still end up in `link.Elf.linkWithLLD` to do the actual linking. The logic for invoking LLD should probably also be unified at least somewhat; I haven't done that in this commit.
1 parent 424e6ac
src/codegen/spirv/Section.zig
@@ -386,8 +386,6 @@ test "SPIR-V Section emit() - string" {
 }
 
 test "SPIR-V Section emit() - extended mask" {
-    if (@import("builtin").zig_backend == .stage1) return error.SkipZigTest;
-
     var section = Section{};
     defer section.deinit(std.testing.allocator);
 
src/codegen/llvm.zig
@@ -1586,6 +1586,24 @@ pub const Object = struct {
         const global_index = self.nav_map.get(nav_index).?;
         const comp = zcu.comp;
 
+        // If we're on COFF and linking with LLD, the linker cares about our exports to determine the subsystem in use.
+        if (comp.bin_file != null and
+            comp.bin_file.?.tag == .coff and
+            zcu.comp.config.use_lld and
+            ip.isFunctionType(ip.getNav(nav_index).typeOf(ip)))
+        {
+            const flags = &comp.bin_file.?.cast(.coff).?.lld_export_flags;
+            for (export_indices) |export_index| {
+                const name = export_index.ptr(zcu).opts.name;
+                if (name.eqlSlice("main", ip)) flags.c_main = true;
+                if (name.eqlSlice("WinMain", ip)) flags.winmain = true;
+                if (name.eqlSlice("wWinMain", ip)) flags.wwinmain = true;
+                if (name.eqlSlice("WinMainCRTStartup", ip)) flags.winmain_crt_startup = true;
+                if (name.eqlSlice("wWinMainCRTStartup", ip)) flags.wwinmain_crt_startup = true;
+                if (name.eqlSlice("DllMainCRTStartup", ip)) flags.dllmain_crt_startup = true;
+            }
+        }
+
         if (export_indices.len != 0) {
             return updateExportedGlobal(self, zcu, global_index, export_indices);
         } else {
src/link/Elf/ZigObject.zig
@@ -310,7 +310,7 @@ pub fn flush(self: *ZigObject, elf_file: *Elf, tid: Zcu.PerThread.Id) !void {
     if (self.dwarf) |*dwarf| {
         const pt: Zcu.PerThread = .activate(elf_file.base.comp.zcu.?, tid);
         defer pt.deactivate();
-        try dwarf.flushModule(pt);
+        try dwarf.flushZcu(pt);
 
         const gpa = elf_file.base.comp.gpa;
         const cpu_arch = elf_file.getTarget().cpu.arch;
@@ -481,7 +481,7 @@ pub fn flush(self: *ZigObject, elf_file: *Elf, tid: Zcu.PerThread.Id) !void {
         self.debug_str_section_dirty = false;
     }
 
-    // The point of flushModule() is to commit changes, so in theory, nothing should
+    // The point of flushZcu() is to commit changes, so in theory, nothing should
     // be dirty after this. However, it is possible for some things to remain
     // dirty because they fail to be written in the event of compile errors,
     // such as debug_line_header_dirty and debug_info_header_dirty.
@@ -661,7 +661,7 @@ pub fn scanRelocs(self: *ZigObject, elf_file: *Elf, undefs: anytype) !void {
         if (shdr.sh_type == elf.SHT_NOBITS) continue;
         if (atom_ptr.scanRelocsRequiresCode(elf_file)) {
             // TODO ideally we don't have to fetch the code here.
-            // Perhaps it would make sense to save the code until flushModule where we
+            // Perhaps it would make sense to save the code until flushZcu where we
             // would free all of generated code?
             const code = try self.codeAlloc(elf_file, atom_index);
             defer gpa.free(code);
@@ -1075,7 +1075,7 @@ pub fn getOrCreateMetadataForLazySymbol(
     }
     state_ptr.* = .pending_flush;
     const symbol_index = symbol_index_ptr.*;
-    // anyerror needs to be deferred until flushModule
+    // anyerror needs to be deferred until flushZcu
     if (lazy_sym.ty != .anyerror_type) try self.updateLazySymbol(elf_file, pt, lazy_sym, symbol_index);
     return symbol_index;
 }
src/link/MachO/DebugSymbols.zig
@@ -178,7 +178,7 @@ fn findFreeSpace(self: *DebugSymbols, object_size: u64, min_alignment: u64) !u64
     return offset;
 }
 
-pub fn flushModule(self: *DebugSymbols, macho_file: *MachO) !void {
+pub fn flushZcu(self: *DebugSymbols, macho_file: *MachO) !void {
     const zo = macho_file.getZigObject().?;
     for (self.relocs.items) |*reloc| {
         const sym = zo.symbols.items[reloc.target];
src/link/MachO/ZigObject.zig
@@ -550,7 +550,7 @@ pub fn getInputSection(self: ZigObject, atom: Atom, macho_file: *MachO) macho.se
     return sect;
 }
 
-pub fn flushModule(self: *ZigObject, macho_file: *MachO, tid: Zcu.PerThread.Id) link.File.FlushError!void {
+pub fn flushZcu(self: *ZigObject, macho_file: *MachO, tid: Zcu.PerThread.Id) link.File.FlushError!void {
     const diags = &macho_file.base.comp.link_diags;
 
     // Handle any lazy symbols that were emitted by incremental compilation.
@@ -589,7 +589,7 @@ pub fn flushModule(self: *ZigObject, macho_file: *MachO, tid: Zcu.PerThread.Id)
     if (self.dwarf) |*dwarf| {
         const pt: Zcu.PerThread = .activate(macho_file.base.comp.zcu.?, tid);
         defer pt.deactivate();
-        dwarf.flushModule(pt) catch |err| switch (err) {
+        dwarf.flushZcu(pt) catch |err| switch (err) {
             error.OutOfMemory => return error.OutOfMemory,
             else => |e| return diags.fail("failed to flush dwarf module: {s}", .{@errorName(e)}),
         };
@@ -599,7 +599,7 @@ pub fn flushModule(self: *ZigObject, macho_file: *MachO, tid: Zcu.PerThread.Id)
         self.debug_strtab_dirty = false;
     }
 
-    // The point of flushModule() is to commit changes, so in theory, nothing should
+    // The point of flushZcu() is to commit changes, so in theory, nothing should
     // be dirty after this. However, it is possible for some things to remain
     // dirty because they fail to be written in the event of compile errors,
     // such as debug_line_header_dirty and debug_info_header_dirty.
@@ -1537,7 +1537,7 @@ pub fn getOrCreateMetadataForLazySymbol(
     }
     state_ptr.* = .pending_flush;
     const symbol_index = symbol_index_ptr.*;
-    // anyerror needs to be deferred until flushModule
+    // anyerror needs to be deferred until flushZcu
     if (lazy_sym.ty != .anyerror_type) try self.updateLazySymbol(macho_file, pt, lazy_sym, symbol_index);
     return symbol_index;
 }
src/link/C.zig
@@ -382,7 +382,7 @@ pub fn updateLineNumber(self: *C, pt: Zcu.PerThread, ti_id: InternPool.TrackedIn
 }
 
 pub fn flush(self: *C, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
-    return self.flushModule(arena, tid, prog_node);
+    return self.flushZcu(arena, tid, prog_node);
 }
 
 fn abiDefines(self: *C, target: std.Target) !std.ArrayList(u8) {
@@ -400,7 +400,7 @@ fn abiDefines(self: *C, target: std.Target) !std.ArrayList(u8) {
     return defines;
 }
 
-pub fn flushModule(self: *C, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
+pub fn flushZcu(self: *C, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
     _ = arena; // Has the same lifetime as the call to Compilation.update.
 
     const tracy = trace(@src());
src/link/Coff.zig
@@ -3,9 +3,6 @@
 //! LLD for traditional linking (linking relocatable object files).
 //! LLD is also the default linker for LLVM.
 
-/// If this is not null, an object file is created by LLVM and emitted to zcu_object_sub_path.
-llvm_object: ?LlvmObject.Ptr = null,
-
 base: link.File,
 image_base: u64,
 subsystem: ?std.Target.SubSystem,
@@ -87,6 +84,16 @@ base_relocs: BaseRelocationTable = .{},
 /// Hot-code swapping state.
 hot_state: if (is_hot_update_compatible) HotUpdateState else struct {} = .{},
 
+/// When linking with LLD, these flags are used to determine the subsystem to pass on the LLD command line.
+lld_export_flags: struct {
+    c_main: bool = false,
+    winmain: bool = false,
+    wwinmain: bool = false,
+    winmain_crt_startup: bool = false,
+    wwinmain_crt_startup: bool = false,
+    dllmain_crt_startup: bool = false,
+} = .{},
+
 const is_hot_update_compatible = switch (builtin.target.os.tag) {
     .windows => true,
     else => false,
@@ -302,9 +309,6 @@ pub fn createEmpty(
         .pdb_out_path = options.pdb_out_path,
         .repro = options.repro,
     };
-    if (use_llvm and comp.config.have_zcu) {
-        coff.llvm_object = try LlvmObject.create(arena, comp);
-    }
     errdefer coff.base.destroy();
 
     if (use_lld and (use_llvm or !comp.config.have_zcu)) {
@@ -322,7 +326,6 @@ pub fn createEmpty(
         .mode = link.File.determineMode(use_lld, output_mode, link_mode),
     });
 
-    assert(coff.llvm_object == null);
     const gpa = comp.gpa;
 
     try coff.strtab.buffer.ensureUnusedCapacity(gpa, @sizeOf(u32));
@@ -428,8 +431,6 @@ pub fn open(
 pub fn deinit(coff: *Coff) void {
     const gpa = coff.base.comp.gpa;
 
-    if (coff.llvm_object) |llvm_object| llvm_object.deinit();
-
     for (coff.sections.items(.free_list)) |*free_list| {
         free_list.deinit(gpa);
     }
@@ -1103,9 +1104,6 @@ pub fn updateFunc(
     if (build_options.skip_non_native and builtin.object_format != .coff) {
         @panic("Attempted to compile for object format that was disabled by build configuration");
     }
-    if (coff.llvm_object) |llvm_object| {
-        return llvm_object.updateFunc(pt, func_index, air, liveness);
-    }
     const tracy = trace(@src());
     defer tracy.end();
 
@@ -1205,7 +1203,6 @@ pub fn updateNav(
     if (build_options.skip_non_native and builtin.object_format != .coff) {
         @panic("Attempted to compile for object format that was disabled by build configuration");
     }
-    if (coff.llvm_object) |llvm_object| return llvm_object.updateNav(pt, nav_index);
     const tracy = trace(@src());
     defer tracy.end();
 
@@ -1330,7 +1327,7 @@ pub fn getOrCreateAtomForLazySymbol(
     }
     state_ptr.* = .pending_flush;
     const atom = atom_ptr.*;
-    // anyerror needs to be deferred until flushModule
+    // anyerror needs to be deferred until flushZcu
     if (lazy_sym.ty != .anyerror_type) try coff.updateLazySymbolAtom(pt, lazy_sym, atom, switch (lazy_sym.kind) {
         .code => coff.text_section_index.?,
         .const_data => coff.rdata_section_index.?,
@@ -1463,8 +1460,6 @@ fn updateNavCode(
 }
 
 pub fn freeNav(coff: *Coff, nav_index: InternPool.NavIndex) void {
-    if (coff.llvm_object) |llvm_object| return llvm_object.freeNav(nav_index);
-
     const gpa = coff.base.comp.gpa;
 
     if (coff.decls.fetchOrderedRemove(nav_index)) |const_kv| {
@@ -1485,50 +1480,7 @@ pub fn updateExports(
     }
 
     const zcu = pt.zcu;
-    const ip = &zcu.intern_pool;
-    const comp = coff.base.comp;
-    const target = comp.root_mod.resolved_target.result;
-
-    if (comp.config.use_llvm) {
-        // Even in the case of LLVM, we need to notice certain exported symbols in order to
-        // detect the default subsystem.
-        for (export_indices) |export_idx| {
-            const exp = export_idx.ptr(zcu);
-            const exported_nav_index = switch (exp.exported) {
-                .nav => |nav| nav,
-                .uav => continue,
-            };
-            const exported_nav = ip.getNav(exported_nav_index);
-            const exported_ty = exported_nav.typeOf(ip);
-            if (!ip.isFunctionType(exported_ty)) continue;
-            const c_cc = target.cCallingConvention().?;
-            const winapi_cc: std.builtin.CallingConvention = switch (target.cpu.arch) {
-                .x86 => .{ .x86_stdcall = .{} },
-                else => c_cc,
-            };
-            const exported_cc = Type.fromInterned(exported_ty).fnCallingConvention(zcu);
-            const CcTag = std.builtin.CallingConvention.Tag;
-            if (@as(CcTag, exported_cc) == @as(CcTag, c_cc) and exp.opts.name.eqlSlice("main", ip) and comp.config.link_libc) {
-                zcu.stage1_flags.have_c_main = true;
-            } else if (@as(CcTag, exported_cc) == @as(CcTag, winapi_cc) and target.os.tag == .windows) {
-                if (exp.opts.name.eqlSlice("WinMain", ip)) {
-                    zcu.stage1_flags.have_winmain = true;
-                } else if (exp.opts.name.eqlSlice("wWinMain", ip)) {
-                    zcu.stage1_flags.have_wwinmain = true;
-                } else if (exp.opts.name.eqlSlice("WinMainCRTStartup", ip)) {
-                    zcu.stage1_flags.have_winmain_crt_startup = true;
-                } else if (exp.opts.name.eqlSlice("wWinMainCRTStartup", ip)) {
-                    zcu.stage1_flags.have_wwinmain_crt_startup = true;
-                } else if (exp.opts.name.eqlSlice("DllMainCRTStartup", ip)) {
-                    zcu.stage1_flags.have_dllmain_crt_startup = true;
-                }
-            }
-        }
-    }
-
-    if (coff.llvm_object) |llvm_object| return llvm_object.updateExports(pt, exported, export_indices);
-
-    const gpa = comp.gpa;
+    const gpa = zcu.gpa;
 
     const metadata = switch (exported) {
         .nav => |nav| blk: {
@@ -1621,7 +1573,6 @@ pub fn deleteExport(
     exported: Zcu.Exported,
     name: InternPool.NullTerminatedString,
 ) void {
-    if (coff.llvm_object) |_| return;
     const metadata = switch (exported) {
         .nav => |nav| coff.navs.getPtr(nav),
         .uav => |uav| coff.uavs.getPtr(uav),
@@ -1692,7 +1643,7 @@ pub fn flush(coff: *Coff, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: st
         };
     }
     switch (comp.config.output_mode) {
-        .Exe, .Obj => return coff.flushModule(arena, tid, prog_node),
+        .Exe, .Obj => return coff.flushZcu(arena, tid, prog_node),
         .Lib => return diags.fail("writing lib files not yet implemented for COFF", .{}),
     }
 }
@@ -1711,8 +1662,12 @@ fn linkWithLLD(coff: *Coff, arena: Allocator, tid: Zcu.PerThread.Id, prog_node:
 
     // If there is no Zig code to compile, then we should skip flushing the output file because it
     // will not be part of the linker line anyway.
-    const module_obj_path: ?[]const u8 = if (comp.zcu != null) blk: {
-        try coff.flushModule(arena, tid, prog_node);
+    const module_obj_path: ?[]const u8 = if (comp.zcu) |zcu| blk: {
+        if (zcu.llvm_object == null) {
+            try coff.flushZcu(arena, tid, prog_node);
+        } else {
+            // `Compilation.flush` has already made LLVM emit this object file for us.
+        }
 
         if (fs.path.dirname(full_out_path)) |dirname| {
             break :blk try fs.path.join(arena, &.{ dirname, coff.base.zcu_object_sub_path.? });
@@ -1998,16 +1953,16 @@ fn linkWithLLD(coff: *Coff, arena: Allocator, tid: Zcu.PerThread.Id, prog_node:
             if (coff.subsystem) |explicit| break :blk explicit;
             switch (target.os.tag) {
                 .windows => {
-                    if (comp.zcu) |module| {
-                        if (module.stage1_flags.have_dllmain_crt_startup or is_dyn_lib)
+                    if (comp.zcu != null) {
+                        if (coff.lld_export_flags.dllmain_crt_startup or is_dyn_lib)
                             break :blk null;
-                        if (module.stage1_flags.have_c_main or comp.config.is_test or
-                            module.stage1_flags.have_winmain_crt_startup or
-                            module.stage1_flags.have_wwinmain_crt_startup)
+                        if (coff.lld_export_flags.c_main or comp.config.is_test or
+                            coff.lld_export_flags.winmain_crt_startup or
+                            coff.lld_export_flags.wwinmain_crt_startup)
                         {
                             break :blk .Console;
                         }
-                        if (module.stage1_flags.have_winmain or module.stage1_flags.have_wwinmain)
+                        if (coff.lld_export_flags.winmain or coff.lld_export_flags.wwinmain)
                             break :blk .Windows;
                     }
                 },
@@ -2136,8 +2091,8 @@ fn linkWithLLD(coff: *Coff, arena: Allocator, tid: Zcu.PerThread.Id, prog_node:
                 } else {
                     try argv.append("-NODEFAULTLIB");
                     if (!is_lib and entry_name == null) {
-                        if (comp.zcu) |module| {
-                            if (module.stage1_flags.have_winmain_crt_startup) {
+                        if (comp.zcu != null) {
+                            if (coff.lld_export_flags.winmain_crt_startup) {
                                 try argv.append("-ENTRY:WinMainCRTStartup");
                             } else {
                                 try argv.append("-ENTRY:wWinMainCRTStartup");
@@ -2244,7 +2199,7 @@ fn findLib(arena: Allocator, name: []const u8, lib_directories: []const Director
     return null;
 }
 
-pub fn flushModule(
+pub fn flushZcu(
     coff: *Coff,
     arena: Allocator,
     tid: Zcu.PerThread.Id,
@@ -2256,22 +2211,17 @@ pub fn flushModule(
     const comp = coff.base.comp;
     const diags = &comp.link_diags;
 
-    if (coff.llvm_object) |llvm_object| {
-        try coff.base.emitLlvmObject(arena, llvm_object, prog_node);
-        return;
-    }
-
     const sub_prog_node = prog_node.start("COFF Flush", 0);
     defer sub_prog_node.end();
 
-    return flushModuleInner(coff, arena, tid) catch |err| switch (err) {
+    return flushZcuInner(coff, arena, tid) catch |err| switch (err) {
         error.OutOfMemory => return error.OutOfMemory,
         error.LinkFailure => return error.LinkFailure,
         else => |e| return diags.fail("COFF flush failed: {s}", .{@errorName(e)}),
     };
 }
 
-fn flushModuleInner(coff: *Coff, arena: Allocator, tid: Zcu.PerThread.Id) !void {
+fn flushZcuInner(coff: *Coff, arena: Allocator, tid: Zcu.PerThread.Id) !void {
     _ = arena;
 
     const comp = coff.base.comp;
@@ -2397,7 +2347,6 @@ pub fn getNavVAddr(
     nav_index: InternPool.Nav.Index,
     reloc_info: link.File.RelocInfo,
 ) !u64 {
-    assert(coff.llvm_object == null);
     const zcu = pt.zcu;
     const ip = &zcu.intern_pool;
     const nav = ip.getNav(nav_index);
@@ -2483,8 +2432,6 @@ pub fn getUavVAddr(
     uav: InternPool.Index,
     reloc_info: link.File.RelocInfo,
 ) !u64 {
-    assert(coff.llvm_object == null);
-
     const this_atom_index = coff.uavs.get(uav).?.atom;
     const sym_index = coff.getAtom(this_atom_index).getSymbolIndex().?;
     const atom_index = coff.getAtomIndexForSymbol(.{
@@ -3798,7 +3745,6 @@ const trace = @import("../tracy.zig").trace;
 
 const Air = @import("../Air.zig");
 const Compilation = @import("../Compilation.zig");
-const LlvmObject = @import("../codegen/llvm.zig").Object;
 const Zcu = @import("../Zcu.zig");
 const InternPool = @import("../InternPool.zig");
 const TableSection = @import("table_section.zig").TableSection;
src/link/Dwarf.zig
@@ -4391,7 +4391,7 @@ fn refAbbrevCode(dwarf: *Dwarf, abbrev_code: AbbrevCode) UpdateError!@typeInfo(A
     return @intFromEnum(abbrev_code);
 }
 
-pub fn flushModule(dwarf: *Dwarf, pt: Zcu.PerThread) FlushError!void {
+pub fn flushZcu(dwarf: *Dwarf, pt: Zcu.PerThread) FlushError!void {
     const zcu = pt.zcu;
     const ip = &zcu.intern_pool;
 
src/link/Elf.zig
@@ -32,9 +32,6 @@ entry_name: ?[]const u8,
 
 ptr_width: PtrWidth,
 
-/// If this is not null, an object file is created by LLVM and emitted to zcu_object_sub_path.
-llvm_object: ?LlvmObject.Ptr = null,
-
 /// A list of all input files.
 /// First index is a special "null file". Order is otherwise not observed.
 files: std.MultiArrayList(File.Entry) = .{},
@@ -344,9 +341,6 @@ pub fn createEmpty(
         .print_map = options.print_map,
         .dump_argv_list = .empty,
     };
-    if (use_llvm and comp.config.have_zcu) {
-        self.llvm_object = try LlvmObject.create(arena, comp);
-    }
     errdefer self.base.destroy();
 
     if (use_lld and (use_llvm or !comp.config.have_zcu)) {
@@ -457,8 +451,6 @@ pub fn open(
 pub fn deinit(self: *Elf) void {
     const gpa = self.base.comp.gpa;
 
-    if (self.llvm_object) |llvm_object| llvm_object.deinit();
-
     for (self.file_handles.items) |fh| {
         fh.close();
     }
@@ -515,7 +507,6 @@ pub fn deinit(self: *Elf) void {
 }
 
 pub fn getNavVAddr(self: *Elf, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index, reloc_info: link.File.RelocInfo) !u64 {
-    assert(self.llvm_object == null);
     return self.zigObjectPtr().?.getNavVAddr(self, pt, nav_index, reloc_info);
 }
 
@@ -530,7 +521,6 @@ pub fn lowerUav(
 }
 
 pub fn getUavVAddr(self: *Elf, uav: InternPool.Index, reloc_info: link.File.RelocInfo) !u64 {
-    assert(self.llvm_object == null);
     return self.zigObjectPtr().?.getUavVAddr(self, uav, reloc_info);
 }
 
@@ -805,35 +795,29 @@ pub fn flush(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std
             else => |e| return diags.fail("failed to link with LLD: {s}", .{@errorName(e)}),
         };
     }
-    try self.flushModule(arena, tid, prog_node);
+    try self.flushZcu(arena, tid, prog_node);
 }
 
-pub fn flushModule(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
+pub fn flushZcu(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
     const tracy = trace(@src());
     defer tracy.end();
 
     const comp = self.base.comp;
     const diags = &comp.link_diags;
 
-    if (self.llvm_object) |llvm_object| {
-        try self.base.emitLlvmObject(arena, llvm_object, prog_node);
-        const use_lld = build_options.have_llvm and comp.config.use_lld;
-        if (use_lld) return;
-    }
-
     if (comp.verbose_link) Compilation.dump_argv(self.dump_argv_list.items);
 
     const sub_prog_node = prog_node.start("ELF Flush", 0);
     defer sub_prog_node.end();
 
-    return flushModuleInner(self, arena, tid) catch |err| switch (err) {
+    return flushZcuInner(self, arena, tid) catch |err| switch (err) {
         error.OutOfMemory => return error.OutOfMemory,
         error.LinkFailure => return error.LinkFailure,
         else => |e| return diags.fail("ELF flush failed: {s}", .{@errorName(e)}),
     };
 }
 
-fn flushModuleInner(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id) !void {
+fn flushZcuInner(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id) !void {
     const comp = self.base.comp;
     const gpa = comp.gpa;
     const diags = &comp.link_diags;
@@ -1523,8 +1507,12 @@ fn linkWithLLD(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: s
 
     // If there is no Zig code to compile, then we should skip flushing the output file because it
     // will not be part of the linker line anyway.
-    const module_obj_path: ?[]const u8 = if (comp.zcu != null) blk: {
-        try self.flushModule(arena, tid, prog_node);
+    const module_obj_path: ?[]const u8 = if (comp.zcu) |zcu| blk: {
+        if (zcu.llvm_object == null) {
+            try self.flushZcu(arena, tid, prog_node);
+        } else {
+            // `Compilation.flush` has already made LLVM emit this object file for us.
+        }
 
         if (fs.path.dirname(full_out_path)) |dirname| {
             break :blk try fs.path.join(arena, &.{ dirname, self.base.zcu_object_sub_path.? });
@@ -2385,7 +2373,6 @@ pub fn writeElfHeader(self: *Elf) !void {
 }
 
 pub fn freeNav(self: *Elf, nav: InternPool.Nav.Index) void {
-    if (self.llvm_object) |llvm_object| return llvm_object.freeNav(nav);
     return self.zigObjectPtr().?.freeNav(self, nav);
 }
 
@@ -2399,7 +2386,6 @@ pub fn updateFunc(
     if (build_options.skip_non_native and builtin.object_format != .elf) {
         @panic("Attempted to compile for object format that was disabled by build configuration");
     }
-    if (self.llvm_object) |llvm_object| return llvm_object.updateFunc(pt, func_index, air, liveness);
     return self.zigObjectPtr().?.updateFunc(self, pt, func_index, air, liveness);
 }
 
@@ -2411,7 +2397,6 @@ pub fn updateNav(
     if (build_options.skip_non_native and builtin.object_format != .elf) {
         @panic("Attempted to compile for object format that was disabled by build configuration");
     }
-    if (self.llvm_object) |llvm_object| return llvm_object.updateNav(pt, nav);
     return self.zigObjectPtr().?.updateNav(self, pt, nav);
 }
 
@@ -2423,7 +2408,6 @@ pub fn updateContainerType(
     if (build_options.skip_non_native and builtin.object_format != .elf) {
         @panic("Attempted to compile for object format that was disabled by build configuration");
     }
-    if (self.llvm_object) |_| return;
     const zcu = pt.zcu;
     const gpa = zcu.gpa;
     return self.zigObjectPtr().?.updateContainerType(pt, ty) catch |err| switch (err) {
@@ -2449,12 +2433,10 @@ pub fn updateExports(
     if (build_options.skip_non_native and builtin.object_format != .elf) {
         @panic("Attempted to compile for object format that was disabled by build configuration");
     }
-    if (self.llvm_object) |llvm_object| return llvm_object.updateExports(pt, exported, export_indices);
     return self.zigObjectPtr().?.updateExports(self, pt, exported, export_indices);
 }
 
 pub fn updateLineNumber(self: *Elf, pt: Zcu.PerThread, ti_id: InternPool.TrackedInst.Index) !void {
-    if (self.llvm_object) |_| return;
     return self.zigObjectPtr().?.updateLineNumber(pt, ti_id);
 }
 
@@ -2463,7 +2445,6 @@ pub fn deleteExport(
     exported: Zcu.Exported,
     name: InternPool.NullTerminatedString,
 ) void {
-    if (self.llvm_object) |_| return;
     return self.zigObjectPtr().?.deleteExport(self, exported, name);
 }
 
@@ -5332,7 +5313,6 @@ const GotSection = synthetic_sections.GotSection;
 const GotPltSection = synthetic_sections.GotPltSection;
 const HashSection = synthetic_sections.HashSection;
 const LinkerDefined = @import("Elf/LinkerDefined.zig");
-const LlvmObject = @import("../codegen/llvm.zig").Object;
 const Zcu = @import("../Zcu.zig");
 const Object = @import("Elf/Object.zig");
 const InternPool = @import("../InternPool.zig");
src/link/Goff.zig
@@ -17,10 +17,8 @@ const link = @import("../link.zig");
 const trace = @import("../tracy.zig").trace;
 const build_options = @import("build_options");
 const Air = @import("../Air.zig");
-const LlvmObject = @import("../codegen/llvm.zig").Object;
 
 base: link.File,
-llvm_object: LlvmObject.Ptr,
 
 pub fn createEmpty(
     arena: Allocator,
@@ -36,7 +34,6 @@ pub fn createEmpty(
     assert(!use_lld); // Caught by Compilation.Config.resolve.
     assert(target.os.tag == .zos); // Caught by Compilation.Config.resolve.
 
-    const llvm_object = try LlvmObject.create(arena, comp);
     const goff = try arena.create(Goff);
     goff.* = .{
         .base = .{
@@ -52,7 +49,6 @@ pub fn createEmpty(
             .disable_lld_caching = options.disable_lld_caching,
             .build_id = options.build_id,
         },
-        .llvm_object = llvm_object,
     };
 
     return goff;
@@ -70,7 +66,7 @@ pub fn open(
 }
 
 pub fn deinit(self: *Goff) void {
-    self.llvm_object.deinit();
+    _ = self;
 }
 
 pub fn updateFunc(
@@ -80,17 +76,19 @@ pub fn updateFunc(
     air: Air,
     liveness: Air.Liveness,
 ) link.File.UpdateNavError!void {
-    if (build_options.skip_non_native and builtin.object_format != .goff)
-        @panic("Attempted to compile for object format that was disabled by build configuration");
-
-    try self.llvm_object.updateFunc(pt, func_index, air, liveness);
+    _ = self;
+    _ = pt;
+    _ = func_index;
+    _ = air;
+    _ = liveness;
+    unreachable; // we always use llvm
 }
 
 pub fn updateNav(self: *Goff, pt: Zcu.PerThread, nav: InternPool.Nav.Index) link.File.UpdateNavError!void {
-    if (build_options.skip_non_native and builtin.object_format != .goff)
-        @panic("Attempted to compile for object format that was disabled by build configuration");
-
-    return self.llvm_object.updateNav(pt, nav);
+    _ = self;
+    _ = pt;
+    _ = nav;
+    unreachable; // we always use llvm
 }
 
 pub fn updateExports(
@@ -99,21 +97,21 @@ pub fn updateExports(
     exported: Zcu.Exported,
     export_indices: []const Zcu.Export.Index,
 ) !void {
-    if (build_options.skip_non_native and builtin.object_format != .goff)
-        @panic("Attempted to compile for object format that was disabled by build configuration");
-
-    return self.llvm_object.updateExports(pt, exported, export_indices);
+    _ = self;
+    _ = pt;
+    _ = exported;
+    _ = export_indices;
+    unreachable; // we always use llvm
 }
 
 pub fn flush(self: *Goff, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
-    return self.flushModule(arena, tid, prog_node);
+    return self.flushZcu(arena, tid, prog_node);
 }
 
-pub fn flushModule(self: *Goff, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
-    if (build_options.skip_non_native and builtin.object_format != .goff)
-        @panic("Attempted to compile for object format that was disabled by build configuration");
-
+pub fn flushZcu(self: *Goff, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
+    _ = self;
+    _ = arena;
     _ = tid;
-
-    try self.base.emitLlvmObject(arena, self.llvm_object, prog_node);
+    _ = prog_node;
+    unreachable; // we always use llvm
 }
src/link/MachO.zig
@@ -6,9 +6,6 @@ base: link.File,
 
 rpath_list: []const []const u8,
 
-/// If this is not null, an object file is created by LLVM and emitted to zcu_object_sub_path.
-llvm_object: ?LlvmObject.Ptr = null,
-
 /// Debug symbols bundle (or dSym).
 d_sym: ?DebugSymbols = null,
 
@@ -225,9 +222,6 @@ pub fn createEmpty(
         .force_load_objc = options.force_load_objc,
         .discard_local_symbols = options.discard_local_symbols,
     };
-    if (use_llvm and comp.config.have_zcu) {
-        self.llvm_object = try LlvmObject.create(arena, comp);
-    }
     errdefer self.base.destroy();
 
     self.base.file = try emit.root_dir.handle.createFile(emit.sub_path, .{
@@ -280,8 +274,6 @@ pub fn open(
 pub fn deinit(self: *MachO) void {
     const gpa = self.base.comp.gpa;
 
-    if (self.llvm_object) |llvm_object| llvm_object.deinit();
-
     if (self.d_sym) |*d_sym| {
         d_sym.deinit();
     }
@@ -350,10 +342,10 @@ pub fn flush(
     tid: Zcu.PerThread.Id,
     prog_node: std.Progress.Node,
 ) link.File.FlushError!void {
-    try self.flushModule(arena, tid, prog_node);
+    try self.flushZcu(arena, tid, prog_node);
 }
 
-pub fn flushModule(
+pub fn flushZcu(
     self: *MachO,
     arena: Allocator,
     tid: Zcu.PerThread.Id,
@@ -366,10 +358,6 @@ pub fn flushModule(
     const gpa = comp.gpa;
     const diags = &self.base.comp.link_diags;
 
-    if (self.llvm_object) |llvm_object| {
-        try self.base.emitLlvmObject(arena, llvm_object, prog_node);
-    }
-
     const sub_prog_node = prog_node.start("MachO Flush", 0);
     defer sub_prog_node.end();
 
@@ -385,7 +373,7 @@ pub fn flushModule(
     // --verbose-link
     if (comp.verbose_link) try self.dumpArgv(comp);
 
-    if (self.getZigObject()) |zo| try zo.flushModule(self, tid);
+    if (self.getZigObject()) |zo| try zo.flushZcu(self, tid);
     if (self.base.isStaticLib()) return relocatable.flushStaticLib(self, comp, module_obj_path);
     if (self.base.isObject()) return relocatable.flushObject(self, comp, module_obj_path);
 
@@ -629,7 +617,7 @@ pub fn flushModule(
         error.LinkFailure => return error.LinkFailure,
         else => |e| return diags.fail("failed to calculate and write uuid: {s}", .{@errorName(e)}),
     };
-    if (self.getDebugSymbols()) |dsym| dsym.flushModule(self) catch |err| switch (err) {
+    if (self.getDebugSymbols()) |dsym| dsym.flushZcu(self) catch |err| switch (err) {
         error.OutOfMemory => return error.OutOfMemory,
         else => |e| return diags.fail("failed to get debug symbols: {s}", .{@errorName(e)}),
     };
@@ -3079,7 +3067,6 @@ pub fn updateFunc(
     if (build_options.skip_non_native and builtin.object_format != .macho) {
         @panic("Attempted to compile for object format that was disabled by build configuration");
     }
-    if (self.llvm_object) |llvm_object| return llvm_object.updateFunc(pt, func_index, air, liveness);
     return self.getZigObject().?.updateFunc(self, pt, func_index, air, liveness);
 }
 
@@ -3087,12 +3074,10 @@ pub fn updateNav(self: *MachO, pt: Zcu.PerThread, nav: InternPool.Nav.Index) lin
     if (build_options.skip_non_native and builtin.object_format != .macho) {
         @panic("Attempted to compile for object format that was disabled by build configuration");
     }
-    if (self.llvm_object) |llvm_object| return llvm_object.updateNav(pt, nav);
     return self.getZigObject().?.updateNav(self, pt, nav);
 }
 
 pub fn updateLineNumber(self: *MachO, pt: Zcu.PerThread, ti_id: InternPool.TrackedInst.Index) !void {
-    if (self.llvm_object) |_| return;
     return self.getZigObject().?.updateLineNumber(pt, ti_id);
 }
 
@@ -3105,7 +3090,6 @@ pub fn updateExports(
     if (build_options.skip_non_native and builtin.object_format != .macho) {
         @panic("Attempted to compile for object format that was disabled by build configuration");
     }
-    if (self.llvm_object) |llvm_object| return llvm_object.updateExports(pt, exported, export_indices);
     return self.getZigObject().?.updateExports(self, pt, exported, export_indices);
 }
 
@@ -3114,17 +3098,14 @@ pub fn deleteExport(
     exported: Zcu.Exported,
     name: InternPool.NullTerminatedString,
 ) void {
-    if (self.llvm_object) |_| return;
     return self.getZigObject().?.deleteExport(self, exported, name);
 }
 
 pub fn freeNav(self: *MachO, nav: InternPool.Nav.Index) void {
-    if (self.llvm_object) |llvm_object| return llvm_object.freeNav(nav);
     return self.getZigObject().?.freeNav(nav);
 }
 
 pub fn getNavVAddr(self: *MachO, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index, reloc_info: link.File.RelocInfo) !u64 {
-    assert(self.llvm_object == null);
     return self.getZigObject().?.getNavVAddr(self, pt, nav_index, reloc_info);
 }
 
@@ -3139,7 +3120,6 @@ pub fn lowerUav(
 }
 
 pub fn getUavVAddr(self: *MachO, uav: InternPool.Index, reloc_info: link.File.RelocInfo) !u64 {
-    assert(self.llvm_object == null);
     return self.getZigObject().?.getUavVAddr(self, uav, reloc_info);
 }
 
@@ -5496,7 +5476,6 @@ const ObjcStubsSection = synthetic.ObjcStubsSection;
 const Object = @import("MachO/Object.zig");
 const LazyBind = bind.LazyBind;
 const LaSymbolPtrSection = synthetic.LaSymbolPtrSection;
-const LlvmObject = @import("../codegen/llvm.zig").Object;
 const Md5 = std.crypto.hash.Md5;
 const Zcu = @import("../Zcu.zig");
 const InternPool = @import("../InternPool.zig");
src/link/Plan9.zig
@@ -494,7 +494,7 @@ fn updateFinish(self: *Plan9, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index
     // write the symbol
     // we already have the got index
     const sym: aout.Sym = .{
-        .value = undefined, // the value of stuff gets filled in in flushModule
+        .value = undefined, // the value of stuff gets filled in in flushZcu
         .type = atom.type,
         .name = try gpa.dupe(u8, nav.name.toSlice(ip)),
     };
@@ -543,7 +543,7 @@ pub fn flush(
         .Obj => return diags.fail("writing plan9 object files unimplemented", .{}),
         .Lib => return diags.fail("writing plan9 lib files unimplemented", .{}),
     }
-    return self.flushModule(arena, tid, prog_node);
+    return self.flushZcu(arena, tid, prog_node);
 }
 
 pub fn changeLine(l: *std.ArrayList(u8), delta_line: i32) !void {
@@ -586,7 +586,7 @@ fn atomCount(self: *Plan9) usize {
     return data_nav_count + fn_nav_count + lazy_atom_count + extern_atom_count + uav_atom_count;
 }
 
-pub fn flushModule(
+pub fn flushZcu(
     self: *Plan9,
     arena: Allocator,
     /// TODO: stop using this
@@ -610,7 +610,7 @@ pub fn flushModule(
     const sub_prog_node = prog_node.start("Flush Module", 0);
     defer sub_prog_node.end();
 
-    log.debug("flushModule", .{});
+    log.debug("flushZcu", .{});
 
     defer assert(self.hdr.entry != 0x0);
 
@@ -1039,7 +1039,7 @@ pub fn getOrCreateAtomForLazySymbol(self: *Plan9, pt: Zcu.PerThread, lazy_sym: F
     const atom = atom_ptr.*;
     _ = try self.getAtomPtr(atom).getOrCreateSymbolTableEntry(self);
     _ = self.getAtomPtr(atom).getOrCreateOffsetTableEntry(self);
-    // anyerror needs to be deferred until flushModule
+    // anyerror needs to be deferred until flushZcu
     if (lazy_sym.ty != .anyerror_type) try self.updateLazySymbolAtom(pt, lazy_sym, atom);
     return atom;
 }
src/link/SpirV.zig
@@ -17,7 +17,7 @@
 //! All regular functions.
 
 // Because SPIR-V requires re-compilation anyway, and so hot swapping will not work
-// anyway, we simply generate all the code in flushModule. This keeps
+// anyway, we simply generate all the code in flushZcu. This keeps
 // things considerably simpler.
 
 const SpirV = @This();
@@ -194,17 +194,17 @@ pub fn updateExports(
 }
 
 pub fn flush(self: *SpirV, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
-    return self.flushModule(arena, tid, prog_node);
+    return self.flushZcu(arena, tid, prog_node);
 }
 
-pub fn flushModule(
+pub fn flushZcu(
     self: *SpirV,
     arena: Allocator,
     tid: Zcu.PerThread.Id,
     prog_node: std.Progress.Node,
 ) link.File.FlushError!void {
     // The goal is to never use this because it's only needed if we need to
-    // write to InternPool, but flushModule is too late to be writing to the
+    // write to InternPool, but flushZcu is too late to be writing to the
     // InternPool.
     _ = tid;
 
src/link/Wasm.zig
@@ -36,7 +36,6 @@ const abi = @import("../arch/wasm/abi.zig");
 const Compilation = @import("../Compilation.zig");
 const Dwarf = @import("Dwarf.zig");
 const InternPool = @import("../InternPool.zig");
-const LlvmObject = @import("../codegen/llvm.zig").Object;
 const Zcu = @import("../Zcu.zig");
 const codegen = @import("../codegen.zig");
 const dev = @import("../dev.zig");
@@ -81,8 +80,6 @@ import_table: bool,
 export_table: bool,
 /// Output name of the file
 name: []const u8,
-/// If this is not null, an object file is created by LLVM and linked with LLD afterwards.
-llvm_object: ?LlvmObject.Ptr = null,
 /// List of relocatable files to be linked into the final binary.
 objects: std.ArrayListUnmanaged(Object) = .{},
 
@@ -2992,9 +2989,6 @@ pub fn createEmpty(
         .object_host_name = .none,
         .preloaded_strings = undefined,
     };
-    if (use_llvm and comp.config.have_zcu) {
-        wasm.llvm_object = try LlvmObject.create(arena, comp);
-    }
     errdefer wasm.base.destroy();
 
     if (options.object_host_name) |name| wasm.object_host_name = (try wasm.internString(name)).toOptional();
@@ -3116,7 +3110,6 @@ fn parseArchive(wasm: *Wasm, obj: link.Input.Object) !void {
 
 pub fn deinit(wasm: *Wasm) void {
     const gpa = wasm.base.comp.gpa;
-    if (wasm.llvm_object) |llvm_object| llvm_object.deinit();
 
     wasm.navs_exe.deinit(gpa);
     wasm.navs_obj.deinit(gpa);
@@ -3196,7 +3189,6 @@ pub fn updateFunc(wasm: *Wasm, pt: Zcu.PerThread, func_index: InternPool.Index,
     if (build_options.skip_non_native and builtin.object_format != .wasm) {
         @panic("Attempted to compile for object format that was disabled by build configuration");
     }
-    if (wasm.llvm_object) |llvm_object| return llvm_object.updateFunc(pt, func_index, air, liveness);
 
     dev.check(.wasm_backend);
 
@@ -3228,7 +3220,6 @@ pub fn updateNav(wasm: *Wasm, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index
     if (build_options.skip_non_native and builtin.object_format != .wasm) {
         @panic("Attempted to compile for object format that was disabled by build configuration");
     }
-    if (wasm.llvm_object) |llvm_object| return llvm_object.updateNav(pt, nav_index);
     const zcu = pt.zcu;
     const ip = &zcu.intern_pool;
     const nav = ip.getNav(nav_index);
@@ -3308,8 +3299,6 @@ pub fn deleteExport(
     exported: Zcu.Exported,
     name: InternPool.NullTerminatedString,
 ) void {
-    if (wasm.llvm_object != null) return;
-
     const zcu = wasm.base.comp.zcu.?;
     const ip = &zcu.intern_pool;
     const name_slice = name.toSlice(ip);
@@ -3332,7 +3321,6 @@ pub fn updateExports(
     if (build_options.skip_non_native and builtin.object_format != .wasm) {
         @panic("Attempted to compile for object format that was disabled by build configuration");
     }
-    if (wasm.llvm_object) |llvm_object| return llvm_object.updateExports(pt, exported, export_indices);
 
     const zcu = pt.zcu;
     const gpa = zcu.gpa;
@@ -3391,7 +3379,7 @@ pub fn flush(wasm: *Wasm, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: st
             else => |e| return diags.fail("failed to link with LLD: {s}", .{@errorName(e)}),
         };
     }
-    return wasm.flushModule(arena, tid, prog_node);
+    return wasm.flushZcu(arena, tid, prog_node);
 }
 
 pub fn prelink(wasm: *Wasm, prog_node: std.Progress.Node) link.File.FlushError!void {
@@ -3785,26 +3773,20 @@ fn markTable(wasm: *Wasm, i: ObjectTableIndex) link.File.FlushError!void {
     try wasm.tables.put(wasm.base.comp.gpa, .fromObjectTable(i), {});
 }
 
-pub fn flushModule(
+pub fn flushZcu(
     wasm: *Wasm,
     arena: Allocator,
     tid: Zcu.PerThread.Id,
     prog_node: std.Progress.Node,
 ) link.File.FlushError!void {
     // The goal is to never use this because it's only needed if we need to
-    // write to InternPool, but flushModule is too late to be writing to the
+    // write to InternPool, but flushZcu is too late to be writing to the
     // InternPool.
     _ = tid;
     const comp = wasm.base.comp;
-    const use_lld = build_options.have_llvm and comp.config.use_lld;
     const diags = &comp.link_diags;
     const gpa = comp.gpa;
 
-    if (wasm.llvm_object) |llvm_object| {
-        try wasm.base.emitLlvmObject(arena, llvm_object, prog_node);
-        if (use_lld) return;
-    }
-
     if (comp.verbose_link) Compilation.dump_argv(wasm.dump_argv_list.items);
 
     if (wasm.base.zcu_object_sub_path) |path| {
@@ -3870,8 +3852,12 @@ fn linkWithLLD(wasm: *Wasm, arena: Allocator, tid: Zcu.PerThread.Id, prog_node:
 
     // If there is no Zig code to compile, then we should skip flushing the output file because it
     // will not be part of the linker line anyway.
-    const module_obj_path: ?[]const u8 = if (comp.zcu != null) blk: {
-        try wasm.flushModule(arena, tid, prog_node);
+    const module_obj_path: ?[]const u8 = if (comp.zcu) |zcu| blk: {
+        if (zcu.llvm_object == null) {
+            try wasm.flushZcu(arena, tid, prog_node);
+        } else {
+            // `Compilation.flush` has already made LLVM emit this object file for us.
+        }
 
         if (fs.path.dirname(full_out_path)) |dirname| {
             break :blk try fs.path.join(arena, &.{ dirname, wasm.base.zcu_object_sub_path.? });
src/link/Xcoff.zig
@@ -17,10 +17,8 @@ const link = @import("../link.zig");
 const trace = @import("../tracy.zig").trace;
 const build_options = @import("build_options");
 const Air = @import("../Air.zig");
-const LlvmObject = @import("../codegen/llvm.zig").Object;
 
 base: link.File,
-llvm_object: LlvmObject.Ptr,
 
 pub fn createEmpty(
     arena: Allocator,
@@ -36,7 +34,6 @@ pub fn createEmpty(
     assert(!use_lld); // Caught by Compilation.Config.resolve.
     assert(target.os.tag == .aix); // Caught by Compilation.Config.resolve.
 
-    const llvm_object = try LlvmObject.create(arena, comp);
     const xcoff = try arena.create(Xcoff);
     xcoff.* = .{
         .base = .{
@@ -52,7 +49,6 @@ pub fn createEmpty(
             .disable_lld_caching = options.disable_lld_caching,
             .build_id = options.build_id,
         },
-        .llvm_object = llvm_object,
     };
 
     return xcoff;
@@ -70,7 +66,7 @@ pub fn open(
 }
 
 pub fn deinit(self: *Xcoff) void {
-    self.llvm_object.deinit();
+    _ = self;
 }
 
 pub fn updateFunc(
@@ -80,17 +76,19 @@ pub fn updateFunc(
     air: Air,
     liveness: Air.Liveness,
 ) link.File.UpdateNavError!void {
-    if (build_options.skip_non_native and builtin.object_format != .xcoff)
-        @panic("Attempted to compile for object format that was disabled by build configuration");
-
-    try self.llvm_object.updateFunc(pt, func_index, air, liveness);
+    _ = self;
+    _ = pt;
+    _ = func_index;
+    _ = air;
+    _ = liveness;
+    unreachable; // we always use llvm
 }
 
 pub fn updateNav(self: *Xcoff, pt: Zcu.PerThread, nav: InternPool.Nav.Index) link.File.UpdateNavError!void {
-    if (build_options.skip_non_native and builtin.object_format != .xcoff)
-        @panic("Attempted to compile for object format that was disabled by build configuration");
-
-    return self.llvm_object.updateNav(pt, nav);
+    _ = self;
+    _ = pt;
+    _ = nav;
+    unreachable; // we always use llvm
 }
 
 pub fn updateExports(
@@ -99,21 +97,21 @@ pub fn updateExports(
     exported: Zcu.Exported,
     export_indices: []const Zcu.Export.Index,
 ) !void {
-    if (build_options.skip_non_native and builtin.object_format != .xcoff)
-        @panic("Attempted to compile for object format that was disabled by build configuration");
-
-    return self.llvm_object.updateExports(pt, exported, export_indices);
+    _ = self;
+    _ = pt;
+    _ = exported;
+    _ = export_indices;
+    unreachable; // we always use llvm
 }
 
 pub fn flush(self: *Xcoff, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
-    return self.flushModule(arena, tid, prog_node);
+    return self.flushZcu(arena, tid, prog_node);
 }
 
-pub fn flushModule(self: *Xcoff, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
-    if (build_options.skip_non_native and builtin.object_format != .xcoff)
-        @panic("Attempted to compile for object format that was disabled by build configuration");
-
+pub fn flushZcu(self: *Xcoff, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
+    _ = self;
+    _ = arena;
     _ = tid;
-
-    try self.base.emitLlvmObject(arena, self.llvm_object, prog_node);
+    _ = prog_node;
+    unreachable; // we always use llvm
 }
src/Zcu/PerThread.zig
@@ -1784,8 +1784,12 @@ pub fn linkerUpdateFunc(pt: Zcu.PerThread, func_index: InternPool.Index, air: *A
         };
     }
 
-    if (comp.bin_file) |lf| {
-        lf.updateFunc(pt, func_index, air.*, liveness) catch |err| switch (err) {
+    if (zcu.llvm_object) |llvm_object| {
+        llvm_object.updateFunc(pt, func_index, air.*, liveness) catch |err| switch (err) {
+            error.OutOfMemory => return error.OutOfMemory,
+        };
+    } else if (comp.bin_file) |lf| {
+        lf.updateFunc(pt, func_index, air, liveness) catch |err| switch (err) {
             error.OutOfMemory => return error.OutOfMemory,
             error.CodegenFail => assert(zcu.failed_codegen.contains(nav_index)),
             error.Overflow, error.RelocationNotByteAligned => {
@@ -1798,10 +1802,6 @@ pub fn linkerUpdateFunc(pt: Zcu.PerThread, func_index: InternPool.Index, air: *A
                 // Not a retryable failure.
             },
         };
-    } else if (zcu.llvm_object) |llvm_object| {
-        llvm_object.updateFunc(pt, func_index, air.*, liveness) catch |err| switch (err) {
-            error.OutOfMemory => return error.OutOfMemory,
-        };
     }
 }
 
@@ -1877,7 +1877,6 @@ fn createFileRootStruct(
     try pt.scanNamespace(namespace_index, decls);
     try zcu.comp.queueJob(.{ .resolve_type_fully = wip_ty.index });
     codegen_type: {
-        if (zcu.comp.config.use_llvm) break :codegen_type;
         if (file.mod.?.strip) break :codegen_type;
         // This job depends on any resolve_type_fully jobs queued up before it.
         try zcu.comp.queueJob(.{ .link_type = wip_ty.index });
@@ -3309,10 +3308,10 @@ fn processExportsInner(
         .uav => {},
     }
 
-    if (zcu.comp.bin_file) |lf| {
-        try zcu.handleUpdateExports(export_indices, lf.updateExports(pt, exported, export_indices));
-    } else if (zcu.llvm_object) |llvm_object| {
+    if (zcu.llvm_object) |llvm_object| {
         try zcu.handleUpdateExports(export_indices, llvm_object.updateExports(pt, exported, export_indices));
+    } else if (zcu.comp.bin_file) |lf| {
+        try zcu.handleUpdateExports(export_indices, lf.updateExports(pt, exported, export_indices));
     }
 }
 
@@ -4064,7 +4063,6 @@ fn recreateStructType(
     try zcu.comp.queueJob(.{ .resolve_type_fully = wip_ty.index });
 
     codegen_type: {
-        if (zcu.comp.config.use_llvm) break :codegen_type;
         if (file.mod.?.strip) break :codegen_type;
         // This job depends on any resolve_type_fully jobs queued up before it.
         try zcu.comp.queueJob(.{ .link_type = wip_ty.index });
@@ -4157,7 +4155,6 @@ fn recreateUnionType(
     try zcu.comp.queueJob(.{ .resolve_type_fully = wip_ty.index });
 
     codegen_type: {
-        if (zcu.comp.config.use_llvm) break :codegen_type;
         if (file.mod.?.strip) break :codegen_type;
         // This job depends on any resolve_type_fully jobs queued up before it.
         try zcu.comp.queueJob(.{ .link_type = wip_ty.index });
src/Compilation.zig
@@ -2188,14 +2188,10 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
             },
         }
 
-        // Handle the case of e.g. -fno-emit-bin -femit-llvm-ir.
-        if (options.emit_bin == null and (comp.verbose_llvm_ir != null or
-            comp.verbose_llvm_bc != null or
-            (use_llvm and comp.emit_asm != null) or
-            comp.emit_llvm_ir != null or
-            comp.emit_llvm_bc != null))
-        {
-            if (opt_zcu) |zcu| zcu.llvm_object = try LlvmObject.create(arena, comp);
+        if (use_llvm) {
+            if (opt_zcu) |zcu| {
+                zcu.llvm_object = try LlvmObject.create(arena, comp);
+            }
         }
 
         break :comp comp;
@@ -2945,6 +2941,33 @@ fn flush(
     tid: Zcu.PerThread.Id,
     prog_node: std.Progress.Node,
 ) !void {
+    if (comp.zcu) |zcu| {
+        if (zcu.llvm_object) |llvm_object| {
+            // Emit the ZCU object from LLVM now; it's required to flush the output file.
+            // If there's an output file, it wants to decide where the LLVM object goes!
+            const zcu_obj_emit_loc: ?EmitLoc = if (comp.bin_file) |lf| .{
+                .directory = null,
+                .basename = lf.zcu_object_sub_path.?,
+            } else null;
+            const sub_prog_node = prog_node.start("LLVM Emit Object", 0);
+            defer sub_prog_node.end();
+            try llvm_object.emit(.{
+                .pre_ir_path = comp.verbose_llvm_ir,
+                .pre_bc_path = comp.verbose_llvm_bc,
+                .bin_path = try resolveEmitLoc(arena, default_artifact_directory, zcu_obj_emit_loc),
+                .asm_path = try resolveEmitLoc(arena, default_artifact_directory, comp.emit_asm),
+                .post_ir_path = try resolveEmitLoc(arena, default_artifact_directory, comp.emit_llvm_ir),
+                .post_bc_path = try resolveEmitLoc(arena, default_artifact_directory, comp.emit_llvm_bc),
+
+                .is_debug = comp.root_mod.optimize_mode == .Debug,
+                .is_small = comp.root_mod.optimize_mode == .ReleaseSmall,
+                .time_report = comp.time_report,
+                .sanitize_thread = comp.config.any_sanitize_thread,
+                .fuzz = comp.config.any_fuzz,
+                .lto = comp.config.lto,
+            });
+        }
+    }
     if (comp.bin_file) |lf| {
         // This is needed before reading the error flags.
         lf.flush(arena, tid, prog_node) catch |err| switch (err) {
@@ -2952,13 +2975,8 @@ fn flush(
             error.OutOfMemory => return error.OutOfMemory,
         };
     }
-
     if (comp.zcu) |zcu| {
         try link.File.C.flushEmitH(zcu);
-
-        if (zcu.llvm_object) |llvm_object| {
-            try emitLlvmObject(comp, arena, default_artifact_directory, null, llvm_object, prog_node);
-        }
     }
 }
 
@@ -3233,34 +3251,6 @@ fn emitOthers(comp: *Compilation) void {
     }
 }
 
-pub fn emitLlvmObject(
-    comp: *Compilation,
-    arena: Allocator,
-    default_artifact_directory: Cache.Path,
-    bin_emit_loc: ?EmitLoc,
-    llvm_object: LlvmObject.Ptr,
-    prog_node: std.Progress.Node,
-) !void {
-    const sub_prog_node = prog_node.start("LLVM Emit Object", 0);
-    defer sub_prog_node.end();
-
-    try llvm_object.emit(.{
-        .pre_ir_path = comp.verbose_llvm_ir,
-        .pre_bc_path = comp.verbose_llvm_bc,
-        .bin_path = try resolveEmitLoc(arena, default_artifact_directory, bin_emit_loc),
-        .asm_path = try resolveEmitLoc(arena, default_artifact_directory, comp.emit_asm),
-        .post_ir_path = try resolveEmitLoc(arena, default_artifact_directory, comp.emit_llvm_ir),
-        .post_bc_path = try resolveEmitLoc(arena, default_artifact_directory, comp.emit_llvm_bc),
-
-        .is_debug = comp.root_mod.optimize_mode == .Debug,
-        .is_small = comp.root_mod.optimize_mode == .ReleaseSmall,
-        .time_report = comp.time_report,
-        .sanitize_thread = comp.config.any_sanitize_thread,
-        .fuzz = comp.config.any_fuzz,
-        .lto = comp.config.lto,
-    });
-}
-
 fn resolveEmitLoc(
     arena: Allocator,
     default_artifact_directory: Cache.Path,
src/link.zig
@@ -19,7 +19,6 @@ const Zcu = @import("Zcu.zig");
 const InternPool = @import("InternPool.zig");
 const Type = @import("Type.zig");
 const Value = @import("Value.zig");
-const LlvmObject = @import("codegen/llvm.zig").Object;
 const lldMain = @import("main.zig").lldMain;
 const Package = @import("Package.zig");
 const dev = @import("dev.zig");
@@ -704,7 +703,9 @@ pub const File = struct {
     }
 
     /// May be called before or after updateExports for any given Nav.
+    /// Asserts that the ZCU is not using the LLVM backend.
     fn updateNav(base: *File, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index) UpdateNavError!void {
+        assert(base.comp.zcu.?.llvm_object == null);
         const nav = pt.zcu.intern_pool.getNav(nav_index);
         assert(nav.status == .fully_resolved);
         switch (base.tag) {
@@ -721,7 +722,9 @@ pub const File = struct {
         TypeFailureReported,
     };
 
+    /// Never called when LLVM is codegenning the ZCU.
     fn updateContainerType(base: *File, pt: Zcu.PerThread, ty: InternPool.Index) UpdateContainerTypeError!void {
+        assert(base.comp.zcu.?.llvm_object == null);
         switch (base.tag) {
             else => {},
             inline .elf => |tag| {
@@ -733,6 +736,7 @@ pub const File = struct {
 
     /// May be called before or after updateExports for any given Decl.
     /// TODO: currently `pub` because `Zcu.PerThread` is calling this.
+    /// Never called when LLVM is codegenning the ZCU.
     pub fn updateFunc(
         base: *File,
         pt: Zcu.PerThread,
@@ -740,6 +744,7 @@ pub const File = struct {
         air: Air,
         liveness: Air.Liveness,
     ) UpdateNavError!void {
+        assert(base.comp.zcu.?.llvm_object == null);
         switch (base.tag) {
             inline else => |tag| {
                 dev.check(tag.devFeature());
@@ -756,7 +761,9 @@ pub const File = struct {
 
     /// On an incremental update, fixup the line number of all `Nav`s at the given `TrackedInst`, because
     /// its line number has changed. The ZIR instruction `ti_id` has tag `.declaration`.
+    /// Never called when LLVM is codegenning the ZCU.
     fn updateLineNumber(base: *File, pt: Zcu.PerThread, ti_id: InternPool.TrackedInst.Index) UpdateLineNumberError!void {
+        assert(base.comp.zcu.?.llvm_object == null);
         {
             const ti = ti_id.resolveFull(&pt.zcu.intern_pool).?;
             const file = pt.zcu.fileByIndex(ti.file);
@@ -846,11 +853,13 @@ pub const File = struct {
 
     /// Commit pending changes and write headers. Works based on `effectiveOutputMode`
     /// rather than final output mode.
-    pub fn flushModule(base: *File, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) FlushError!void {
+    /// Never called when LLVM is codegenning the ZCU.
+    fn flushZcu(base: *File, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) FlushError!void {
+        assert(base.comp.zcu.?.llvm_object == null);
         switch (base.tag) {
             inline else => |tag| {
                 dev.check(tag.devFeature());
-                return @as(*tag.Type(), @fieldParentPtr("base", base)).flushModule(arena, tid, prog_node);
+                return @as(*tag.Type(), @fieldParentPtr("base", base)).flushZcu(arena, tid, prog_node);
             },
         }
     }
@@ -864,12 +873,14 @@ pub const File = struct {
     /// a list of size 1, meaning that `exported` is exported once. However, it is possible
     /// to export the same thing with multiple different symbol names (aliases).
     /// May be called before or after updateDecl for any given Decl.
+    /// Never called when LLVM is codegenning the ZCU.
     pub fn updateExports(
         base: *File,
         pt: Zcu.PerThread,
         exported: Zcu.Exported,
         export_indices: []const Zcu.Export.Index,
     ) UpdateExportsError!void {
+        assert(base.comp.zcu.?.llvm_object == null);
         switch (base.tag) {
             inline else => |tag| {
                 dev.check(tag.devFeature());
@@ -896,7 +907,9 @@ pub const File = struct {
     /// `Nav`'s address was not yet resolved, or the containing atom gets moved in virtual memory.
     /// May be called before or after updateFunc/updateNav therefore it is up to the linker to allocate
     /// the block/atom.
+    /// Never called when LLVM is codegenning the ZCU.
     pub fn getNavVAddr(base: *File, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index, reloc_info: RelocInfo) !u64 {
+        assert(base.comp.zcu.?.llvm_object == null);
         switch (base.tag) {
             .c => unreachable,
             .spirv => unreachable,
@@ -909,6 +922,7 @@ pub const File = struct {
         }
     }
 
+    /// Never called when LLVM is codegenning the ZCU.
     pub fn lowerUav(
         base: *File,
         pt: Zcu.PerThread,
@@ -916,6 +930,7 @@ pub const File = struct {
         decl_align: InternPool.Alignment,
         src_loc: Zcu.LazySrcLoc,
     ) !codegen.GenResult {
+        assert(base.comp.zcu.?.llvm_object == null);
         switch (base.tag) {
             .c => unreachable,
             .spirv => unreachable,
@@ -928,7 +943,9 @@ pub const File = struct {
         }
     }
 
+    /// Never called when LLVM is codegenning the ZCU.
     pub fn getUavVAddr(base: *File, decl_val: InternPool.Index, reloc_info: RelocInfo) !u64 {
+        assert(base.comp.zcu.?.llvm_object == null);
         switch (base.tag) {
             .c => unreachable,
             .spirv => unreachable,
@@ -941,11 +958,13 @@ pub const File = struct {
         }
     }
 
+    /// Never called when LLVM is codegenning the ZCU.
     pub fn deleteExport(
         base: *File,
         exported: Zcu.Exported,
         name: InternPool.NullTerminatedString,
     ) void {
+        assert(base.comp.zcu.?.llvm_object == null);
         switch (base.tag) {
             .plan9,
             .spirv,
@@ -1077,7 +1096,7 @@ pub const File = struct {
         }
     }
 
-    pub fn linkAsArchive(base: *File, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) FlushError!void {
+    fn linkAsArchive(base: *File, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) FlushError!void {
         dev.check(.lld_linker);
 
         const tracy = trace(@src());
@@ -1103,9 +1122,12 @@ pub const File = struct {
 
         // If there is no Zig code to compile, then we should skip flushing the output file
         // because it will not be part of the linker line anyway.
-        const zcu_obj_path: ?[]const u8 = if (opt_zcu != null) blk: {
-            try base.flushModule(arena, tid, prog_node);
-
+        const zcu_obj_path: ?[]const u8 = if (opt_zcu) |zcu| blk: {
+            if (zcu.llvm_object == null) {
+                try base.flushZcu(arena, tid, prog_node);
+            } else {
+                // `Compilation.flush` has already made LLVM emit this object file for us.
+            }
             const dirname = fs.path.dirname(full_out_path_z) orelse ".";
             break :blk try fs.path.join(arena, &.{ dirname, base.zcu_object_sub_path.? });
         } else null;
@@ -1346,21 +1368,6 @@ pub const File = struct {
         return output_mode == .Lib and !self.isStatic();
     }
 
-    pub fn emitLlvmObject(
-        base: File,
-        arena: Allocator,
-        llvm_object: LlvmObject.Ptr,
-        prog_node: std.Progress.Node,
-    ) !void {
-        return base.comp.emitLlvmObject(arena, .{
-            .root_dir = base.emit.root_dir,
-            .sub_path = std.fs.path.dirname(base.emit.sub_path) orelse "",
-        }, .{
-            .directory = null,
-            .basename = base.zcu_object_sub_path.?,
-        }, llvm_object, prog_node);
-    }
-
     pub fn cgFail(
         base: *File,
         nav_index: InternPool.Nav.Index,
@@ -1600,7 +1607,11 @@ pub fn doTask(comp: *Compilation, tid: usize, task: Task) void {
                 // on the failed type, so when it is changed the `Nav` will be updated.
                 return;
             }
-            if (comp.bin_file) |lf| {
+            if (zcu.llvm_object) |llvm_object| {
+                llvm_object.updateNav(pt, nav_index) catch |err| switch (err) {
+                    error.OutOfMemory => diags.setAllocFailure(),
+                };
+            } else if (comp.bin_file) |lf| {
                 lf.updateNav(pt, nav_index) catch |err| switch (err) {
                     error.OutOfMemory => diags.setAllocFailure(),
                     error.CodegenFail => assert(zcu.failed_codegen.contains(nav_index)),
@@ -1616,10 +1627,6 @@ pub fn doTask(comp: *Compilation, tid: usize, task: Task) void {
                         // Not a retryable failure.
                     },
                 };
-            } else if (zcu.llvm_object) |llvm_object| {
-                llvm_object.updateNav(pt, nav_index) catch |err| switch (err) {
-                    error.OutOfMemory => diags.setAllocFailure(),
-                };
             }
         },
         .link_func => |func| {
@@ -1650,11 +1657,13 @@ pub fn doTask(comp: *Compilation, tid: usize, task: Task) void {
                 // on the failed type, so when that is changed, this type will be updated.
                 return;
             }
-            if (comp.bin_file) |lf| {
-                lf.updateContainerType(pt, ty) catch |err| switch (err) {
-                    error.OutOfMemory => diags.setAllocFailure(),
-                    error.TypeFailureReported => assert(zcu.failed_types.contains(ty)),
-                };
+            if (zcu.llvm_object == null) {
+                if (comp.bin_file) |lf| {
+                    lf.updateContainerType(pt, ty) catch |err| switch (err) {
+                        error.OutOfMemory => diags.setAllocFailure(),
+                        error.TypeFailureReported => assert(zcu.failed_types.contains(ty)),
+                    };
+                }
             }
         },
         .update_line_number => |ti| {
@@ -1664,11 +1673,13 @@ pub fn doTask(comp: *Compilation, tid: usize, task: Task) void {
             }
             const pt: Zcu.PerThread = .activate(comp.zcu.?, @enumFromInt(tid));
             defer pt.deactivate();
-            if (comp.bin_file) |lf| {
-                lf.updateLineNumber(pt, ti) catch |err| switch (err) {
-                    error.OutOfMemory => diags.setAllocFailure(),
-                    else => |e| log.err("update line number failed: {s}", .{@errorName(e)}),
-                };
+            if (pt.zcu.llvm_object == null) {
+                if (comp.bin_file) |lf| {
+                    lf.updateLineNumber(pt, ti) catch |err| switch (err) {
+                        error.OutOfMemory => diags.setAllocFailure(),
+                        else => |e| log.err("update line number failed: {s}", .{@errorName(e)}),
+                    };
+                }
             }
         },
     }
src/target.zig
@@ -739,7 +739,7 @@ pub fn functionPointerMask(target: std.Target) ?u64 {
 
 pub fn supportsTailCall(target: std.Target, backend: std.builtin.CompilerBackend) bool {
     switch (backend) {
-        .stage1, .stage2_llvm => return @import("codegen/llvm.zig").supportsTailCall(target),
+        .stage2_llvm => return @import("codegen/llvm.zig").supportsTailCall(target),
         .stage2_c => return true,
         else => return false,
     }
src/Zcu.zig
@@ -56,9 +56,8 @@ comptime {
 /// General-purpose allocator. Used for both temporary and long-term storage.
 gpa: Allocator,
 comp: *Compilation,
-/// Usually, the LlvmObject is managed by linker code, however, in the case
-/// that -fno-emit-bin is specified, the linker code never executes, so we
-/// store the LlvmObject here.
+/// If the ZCU is emitting an LLVM object (i.e. we are using the LLVM backend), then this is the
+/// `LlvmObject` we are emitting to.
 llvm_object: ?LlvmObject.Ptr,
 
 /// Pointer to externally managed resource.
@@ -267,16 +266,6 @@ resolved_references: ?std.AutoHashMapUnmanaged(AnalUnit, ?ResolvedReference) = n
 /// Reset to `false` at the start of each update in `Compilation.update`.
 skip_analysis_this_update: bool = false,
 
-stage1_flags: packed struct {
-    have_winmain: bool = false,
-    have_wwinmain: bool = false,
-    have_winmain_crt_startup: bool = false,
-    have_wwinmain_crt_startup: bool = false,
-    have_dllmain_crt_startup: bool = false,
-    have_c_main: bool = false,
-    reserved: u2 = 0,
-} = .{},
-
 test_functions: std.AutoArrayHashMapUnmanaged(InternPool.Nav.Index, void) = .empty,
 
 global_assembly: std.AutoArrayHashMapUnmanaged(AnalUnit, []u8) = .empty,