Commit a5fb28070f

Andrew Kelley <andrew@ziglang.org>
2021-07-22 23:44:06
add -femit-llvm-bc CLI option and implement it
* Added doc comments for `std.Target.ObjectFormat` enum * `std.Target.oFileExt` is removed because it is incorrect for Plan-9 targets. Instead, use `std.Target.ObjectFormat.fileExt` and pass a CPU architecture. * Added `Compilation.Directory.joinZ` for when a null byte is desired. * Improvements to `Compilation.create` logic for computing `use_llvm` and reporting errors in contradictory flags. `-femit-llvm-ir` and `-femit-llvm-bc` will now imply `-fLLVM`. * Fix compilation when passing `.bc` files on the command line. * Improvements to the stage2 LLVM backend: - cleaned up error messages and error reporting. Properly bubble up some errors rather than dumping to stderr; others turn into panics. - properly call ZigLLVMCreateTargetMachine and ZigLLVMTargetMachineEmitToFile and implement calculation of the respective parameters (cpu features, code model, abi name, lto, tsan, etc). - LLVM module verification only runs in debug builds of the compiler - use LLVMDumpModule rather than printToString because in the case that we incorrectly pass a null pointer to LLVM it may crash during dumping the module and having it partially printed is helpful in this case. - support -femit-asm, -fno-emit-bin, -femit-llvm-ir, -femit-llvm-bc - Support LLVM backend when used with Mach-O and WASM linkers.
1 parent 36295d7
doc/docgen.zig
@@ -1,5 +1,5 @@
 const std = @import("std");
-const builtin = std.builtin;
+const builtin = @import("builtin");
 const io = std.io;
 const fs = std.fs;
 const process = std.process;
@@ -13,7 +13,7 @@ const Allocator = std.mem.Allocator;
 const max_doc_file_size = 10 * 1024 * 1024;
 
 const exe_ext = @as(std.zig.CrossTarget, .{}).exeFileExt();
-const obj_ext = @as(std.zig.CrossTarget, .{}).oFileExt();
+const obj_ext = builtin.object_format.fileExt(builtin.cpu.arch);
 const tmp_dir_name = "docgen_tmp";
 const test_out_path = tmp_dir_name ++ fs.path.sep_str ++ "test" ++ exe_ext;
 
@@ -281,7 +281,7 @@ const Code = struct {
     name: []const u8,
     source_token: Token,
     is_inline: bool,
-    mode: builtin.Mode,
+    mode: std.builtin.Mode,
     link_objects: []const []const u8,
     target_str: ?[]const u8,
     link_libc: bool,
@@ -531,7 +531,7 @@ fn genToc(allocator: *Allocator, tokenizer: *Tokenizer) !Toc {
                         return parseError(tokenizer, code_kind_tok, "unrecognized code kind: {s}", .{code_kind_str});
                     }
 
-                    var mode: builtin.Mode = .Debug;
+                    var mode: std.builtin.Mode = .Debug;
                     var link_objects = std.ArrayList([]const u8).init(allocator);
                     defer link_objects.deinit();
                     var target_str: ?[]const u8 = null;
lib/std/zig/cross_target.zig
@@ -473,10 +473,6 @@ pub const CrossTarget = struct {
         return self.getOsTag() == .windows;
     }
 
-    pub fn oFileExt(self: CrossTarget) [:0]const u8 {
-        return Target.oFileExt_os_abi(self.getOsTag(), self.getAbi());
-    }
-
     pub fn exeFileExt(self: CrossTarget) [:0]const u8 {
         return Target.exeFileExtSimple(self.getCpuArch(), self.getOsTag());
     }
lib/std/target.zig
@@ -549,15 +549,36 @@ pub const Target = struct {
     };
 
     pub const ObjectFormat = enum {
+        /// Common Object File Format (Windows)
         coff,
+        /// Executable and Linking Format
         elf,
+        /// macOS relocatables
         macho,
+        /// WebAssembly
         wasm,
+        /// C source code
         c,
+        /// Standard, Portable Intermediate Representation V
         spirv,
+        /// Intel IHEX
         hex,
+        /// Machine code with no metadata.
         raw,
+        /// Plan 9 from Bell Labs
         plan9,
+
+        pub fn fileExt(of: ObjectFormat, cpu_arch: Cpu.Arch) [:0]const u8 {
+            return switch (of) {
+                .coff => ".obj",
+                .elf, .macho, .wasm => ".o",
+                .c => ".c",
+                .spirv => ".spv",
+                .hex => ".ihex",
+                .raw => ".bin",
+                .plan9 => plan9Ext(cpu_arch),
+            };
+        }
     };
 
     pub const SubSystem = enum {
@@ -1289,30 +1310,16 @@ pub const Target = struct {
         return linuxTripleSimple(allocator, self.cpu.arch, self.os.tag, self.abi);
     }
 
-    pub fn oFileExt_os_abi(os_tag: Os.Tag, abi: Abi) [:0]const u8 {
-        if (abi == .msvc) {
-            return ".obj";
-        }
-        switch (os_tag) {
-            .windows, .uefi => return ".obj",
-            else => return ".o",
-        }
-    }
-
-    pub fn oFileExt(self: Target) [:0]const u8 {
-        return oFileExt_os_abi(self.os.tag, self.abi);
-    }
-
     pub fn exeFileExtSimple(cpu_arch: Cpu.Arch, os_tag: Os.Tag) [:0]const u8 {
-        switch (os_tag) {
-            .windows => return ".exe",
-            .uefi => return ".efi",
-            else => if (cpu_arch.isWasm()) {
-                return ".wasm";
-            } else {
-                return "";
+        return switch (os_tag) {
+            .windows => ".exe",
+            .uefi => ".efi",
+            .plan9 => plan9Ext(cpu_arch),
+            else => switch (cpu_arch) {
+                .wasm32, .wasm64 => ".wasm",
+                else => "",
             },
-        }
+        };
     }
 
     pub fn exeFileExt(self: Target) [:0]const u8 {
@@ -1352,20 +1359,16 @@ pub const Target = struct {
     }
 
     pub fn getObjectFormatSimple(os_tag: Os.Tag, cpu_arch: Cpu.Arch) ObjectFormat {
-        if (os_tag == .windows or os_tag == .uefi) {
-            return .coff;
-        } else if (os_tag.isDarwin()) {
-            return .macho;
-        }
-        if (cpu_arch.isWasm()) {
-            return .wasm;
-        }
-        if (cpu_arch.isSPIRV()) {
-            return .spirv;
-        }
-        if (os_tag == .plan9)
-            return .plan9;
-        return .elf;
+        return switch (os_tag) {
+            .windows, .uefi => .coff,
+            .ios, .macos, .watchos, .tvos => .macho,
+            .plan9 => .plan9,
+            else => return switch (cpu_arch) {
+                .wasm32, .wasm64 => .wasm,
+                .spirv32, .spirv64 => .spirv,
+                else => .elf,
+            },
+        };
     }
 
     pub fn getObjectFormat(self: Target) ObjectFormat {
@@ -1676,6 +1679,30 @@ pub const Target = struct {
 
         return false;
     }
+
+    /// 0c spim    little-endian MIPS 3000 family
+    /// 1c 68000   Motorola MC68000
+    /// 2c 68020   Motorola MC68020
+    /// 5c arm     little-endian ARM
+    /// 6c amd64   AMD64 and compatibles (e.g., Intel EM64T)
+    /// 7c arm64   ARM64 (ARMv8)
+    /// 8c 386     Intel i386, i486, Pentium, etc.
+    /// kc sparc   Sun SPARC
+    /// qc power   Power PC
+    /// vc mips    big-endian MIPS 3000 family
+    pub fn plan9Ext(cpu_arch: Cpu.Arch) [:0]const u8 {
+        return switch (cpu_arch) {
+            .arm => ".5",
+            .x86_64 => ".6",
+            .aarch64 => ".7",
+            .i386 => ".8",
+            .sparc => ".k",
+            .powerpc, .powerpcle => ".q",
+            .mips, .mipsel => ".v",
+            // ISAs without designated characters get 'X' for lack of a better option.
+            else => ".X",
+        };
+    }
 };
 
 test {
lib/std/zig.zig
@@ -108,7 +108,8 @@ pub const BinNameOptions = struct {
 pub fn binNameAlloc(allocator: *std.mem.Allocator, options: BinNameOptions) error{OutOfMemory}![]u8 {
     const root_name = options.root_name;
     const target = options.target;
-    switch (options.object_format orelse target.getObjectFormat()) {
+    const ofmt = options.object_format orelse target.getObjectFormat();
+    switch (ofmt) {
         .coff => switch (options.output_mode) {
             .Exe => return std.fmt.allocPrint(allocator, "{s}{s}", .{ root_name, target.exeFileExt() }),
             .Lib => {
@@ -118,7 +119,7 @@ pub fn binNameAlloc(allocator: *std.mem.Allocator, options: BinNameOptions) erro
                 };
                 return std.fmt.allocPrint(allocator, "{s}{s}", .{ root_name, suffix });
             },
-            .Obj => return std.fmt.allocPrint(allocator, "{s}{s}", .{ root_name, target.oFileExt() }),
+            .Obj => return std.fmt.allocPrint(allocator, "{s}.obj", .{root_name}),
         },
         .elf => switch (options.output_mode) {
             .Exe => return allocator.dupe(u8, root_name),
@@ -140,7 +141,7 @@ pub fn binNameAlloc(allocator: *std.mem.Allocator, options: BinNameOptions) erro
                     },
                 }
             },
-            .Obj => return std.fmt.allocPrint(allocator, "{s}{s}", .{ root_name, target.oFileExt() }),
+            .Obj => return std.fmt.allocPrint(allocator, "{s}.o", .{root_name}),
         },
         .macho => switch (options.output_mode) {
             .Exe => return allocator.dupe(u8, root_name),
@@ -163,7 +164,7 @@ pub fn binNameAlloc(allocator: *std.mem.Allocator, options: BinNameOptions) erro
                 }
                 return std.fmt.allocPrint(allocator, "{s}{s}{s}", .{ target.libPrefix(), root_name, suffix });
             },
-            .Obj => return std.fmt.allocPrint(allocator, "{s}{s}", .{ root_name, target.oFileExt() }),
+            .Obj => return std.fmt.allocPrint(allocator, "{s}.o", .{root_name}),
         },
         .wasm => switch (options.output_mode) {
             .Exe => return std.fmt.allocPrint(allocator, "{s}{s}", .{ root_name, target.exeFileExt() }),
@@ -175,36 +176,15 @@ pub fn binNameAlloc(allocator: *std.mem.Allocator, options: BinNameOptions) erro
                     .Dynamic => return std.fmt.allocPrint(allocator, "{s}.wasm", .{root_name}),
                 }
             },
-            .Obj => return std.fmt.allocPrint(allocator, "{s}{s}", .{ root_name, target.oFileExt() }),
+            .Obj => return std.fmt.allocPrint(allocator, "{s}.o", .{root_name}),
         },
         .c => return std.fmt.allocPrint(allocator, "{s}.c", .{root_name}),
         .spirv => return std.fmt.allocPrint(allocator, "{s}.spv", .{root_name}),
         .hex => return std.fmt.allocPrint(allocator, "{s}.ihex", .{root_name}),
         .raw => return std.fmt.allocPrint(allocator, "{s}.bin", .{root_name}),
-        .plan9 => {
-            // copied from 2c(1)
-            // 0c spim    little-endian MIPS 3000 family
-            // 1c 68000   Motorola MC68000
-            // 2c 68020   Motorola MC68020
-            // 5c arm     little-endian ARM
-            // 6c amd64   AMD64 and compatibles (e.g., Intel EM64T)
-            // 7c arm64   ARM64 (ARMv8)
-            // 8c 386     Intel i386, i486, Pentium, etc.
-            // kc sparc   Sun SPARC
-            // qc power   Power PC
-            // vc mips    big-endian MIPS 3000 family
-            const char: u8 = switch (target.cpu.arch) {
-                .arm => '5',
-                .x86_64 => '6',
-                .aarch64 => '7',
-                .i386 => '8',
-                .sparc => 'k',
-                .powerpc, .powerpcle => 'q',
-                .mips, .mipsel => 'v',
-                else => 'X', // this arch does not have a char or maybe was not ported to plan9 so we just use X
-            };
-            return std.fmt.allocPrint(allocator, "{s}.{c}", .{ root_name, char });
-        },
+        .plan9 => return std.fmt.allocPrint(allocator, "{s}{s}", .{
+            root_name, ofmt.fileExt(target.cpu.arch),
+        }),
     }
 }
 
src/codegen/llvm/bindings.zig
@@ -123,6 +123,9 @@ pub const Module = opaque {
 
     pub const getNamedGlobal = LLVMGetNamedGlobal;
     extern fn LLVMGetNamedGlobal(M: *const Module, Name: [*:0]const u8) ?*const Value;
+
+    pub const dump = LLVMDumpModule;
+    extern fn LLVMDumpModule(M: *const Module) void;
 };
 
 pub const lookupIntrinsicID = LLVMLookupIntrinsicID;
@@ -250,31 +253,41 @@ pub const BasicBlock = opaque {
 };
 
 pub const TargetMachine = opaque {
-    pub const create = LLVMCreateTargetMachine;
-    extern fn LLVMCreateTargetMachine(
+    pub const create = ZigLLVMCreateTargetMachine;
+    extern fn ZigLLVMCreateTargetMachine(
         T: *const Target,
         Triple: [*:0]const u8,
-        CPU: [*:0]const u8,
-        Features: [*:0]const u8,
+        CPU: ?[*:0]const u8,
+        Features: ?[*:0]const u8,
         Level: CodeGenOptLevel,
         Reloc: RelocMode,
-        CodeModel: CodeMode,
+        CodeModel: CodeModel,
+        function_sections: bool,
+        float_abi: ABIType,
+        abi_name: ?[*:0]const u8,
     ) *const TargetMachine;
 
     pub const dispose = LLVMDisposeTargetMachine;
     extern fn LLVMDisposeTargetMachine(T: *const TargetMachine) void;
 
-    pub const emitToFile = LLVMTargetMachineEmitToFile;
-    extern fn LLVMTargetMachineEmitToFile(
-        *const TargetMachine,
+    pub const emitToFile = ZigLLVMTargetMachineEmitToFile;
+    extern fn ZigLLVMTargetMachineEmitToFile(
+        T: *const TargetMachine,
         M: *const Module,
-        Filename: [*:0]const u8,
-        codegen: CodeGenFileType,
         ErrorMessage: *[*:0]const u8,
-    ) Bool;
+        is_debug: bool,
+        is_small: bool,
+        time_report: bool,
+        tsan: bool,
+        lto: bool,
+        asm_filename: ?[*:0]const u8,
+        bin_filename: ?[*:0]const u8,
+        llvm_ir_filename: ?[*:0]const u8,
+        bitcode_filename: ?[*:0]const u8,
+    ) bool;
 };
 
-pub const CodeMode = enum(c_int) {
+pub const CodeModel = enum(c_int) {
     Default,
     JITDefault,
     Tiny,
@@ -295,7 +308,7 @@ pub const RelocMode = enum(c_int) {
     Default,
     Static,
     PIC,
-    DynamicNoPic,
+    DynamicNoPIC,
     ROPI,
     RWPI,
     ROPI_RWPI,
@@ -306,6 +319,15 @@ pub const CodeGenFileType = enum(c_int) {
     ObjectFile,
 };
 
+pub const ABIType = enum(c_int) {
+    /// Target-specific (either soft or hard depending on triple, etc).
+    Default,
+    /// Soft float.
+    Soft,
+    // Hard float.
+    Hard,
+};
+
 pub const Target = opaque {
     pub const getFromTriple = LLVMGetTargetFromTriple;
     extern fn LLVMGetTargetFromTriple(Triple: [*:0]const u8, T: **const Target, ErrorMessage: *[*:0]const u8) Bool;
src/codegen/llvm.zig
@@ -72,9 +72,9 @@ pub fn targetTriple(allocator: *Allocator, target: std.Target) ![:0]u8 {
         .renderscript32 => "renderscript32",
         .renderscript64 => "renderscript64",
         .ve => "ve",
-        .spu_2 => return error.LLVMBackendDoesNotSupportSPUMarkII,
-        .spirv32 => return error.LLVMBackendDoesNotSupportSPIRV,
-        .spirv64 => return error.LLVMBackendDoesNotSupportSPIRV,
+        .spu_2 => return error.@"LLVM backend does not support SPU Mark II",
+        .spirv32 => return error.@"LLVM backend does not support SPIR-V",
+        .spirv64 => return error.@"LLVM backend does not support SPIR-V",
     };
 
     const llvm_os = switch (target.os.tag) {
@@ -114,11 +114,13 @@ pub fn targetTriple(allocator: *Allocator, target: std.Target) ![:0]u8 {
         .wasi => "wasi",
         .emscripten => "emscripten",
         .uefi => "windows",
-        .opencl => return error.LLVMBackendDoesNotSupportOpenCL,
-        .glsl450 => return error.LLVMBackendDoesNotSupportGLSL450,
-        .vulkan => return error.LLVMBackendDoesNotSupportVulkan,
-        .plan9 => return error.LLVMBackendDoesNotSupportPlan9,
-        .other => "unknown",
+
+        .opencl,
+        .glsl450,
+        .vulkan,
+        .plan9,
+        .other,
+        => "unknown",
     };
 
     const llvm_abi = switch (target.abi) {
@@ -152,84 +154,105 @@ pub const Object = struct {
     llvm_module: *const llvm.Module,
     context: *const llvm.Context,
     target_machine: *const llvm.TargetMachine,
-    object_pathZ: [:0]const u8,
-
-    pub fn create(allocator: *Allocator, sub_path: []const u8, options: link.Options) !*Object {
-        _ = sub_path;
-        const self = try allocator.create(Object);
-        errdefer allocator.destroy(self);
-
-        const obj_basename = try std.zig.binNameAlloc(allocator, .{
-            .root_name = options.root_name,
-            .target = options.target,
-            .output_mode = .Obj,
-        });
-        defer allocator.free(obj_basename);
 
-        const o_directory = options.module.?.zig_cache_artifact_directory;
-        const object_path = try o_directory.join(allocator, &[_][]const u8{obj_basename});
-        defer allocator.free(object_path);
-
-        const object_pathZ = try allocator.dupeZ(u8, object_path);
-        errdefer allocator.free(object_pathZ);
+    pub fn create(gpa: *Allocator, options: link.Options) !*Object {
+        const obj = try gpa.create(Object);
+        errdefer gpa.destroy(obj);
+        obj.* = try Object.init(gpa, options);
+        return obj;
+    }
 
+    pub fn init(gpa: *Allocator, options: link.Options) !Object {
         const context = llvm.Context.create();
         errdefer context.dispose();
 
         initializeLLVMTargets();
 
-        const root_nameZ = try allocator.dupeZ(u8, options.root_name);
-        defer allocator.free(root_nameZ);
+        const root_nameZ = try gpa.dupeZ(u8, options.root_name);
+        defer gpa.free(root_nameZ);
         const llvm_module = llvm.Module.createWithName(root_nameZ.ptr, context);
         errdefer llvm_module.dispose();
 
-        const llvm_target_triple = try targetTriple(allocator, options.target);
-        defer allocator.free(llvm_target_triple);
+        const llvm_target_triple = try targetTriple(gpa, options.target);
+        defer gpa.free(llvm_target_triple);
 
         var error_message: [*:0]const u8 = undefined;
         var target: *const llvm.Target = undefined;
         if (llvm.Target.getFromTriple(llvm_target_triple.ptr, &target, &error_message).toBool()) {
             defer llvm.disposeMessage(error_message);
 
-            const stderr = std.io.getStdErr().writer();
-            try stderr.print(
-                \\Zig is expecting LLVM to understand this target: '{s}'
-                \\However LLVM responded with: "{s}"
-                \\
-            ,
-                .{ llvm_target_triple, error_message },
-            );
-            return error.InvalidLLVMTriple;
+            log.err("LLVM failed to parse '{s}': {s}", .{ llvm_target_triple, error_message });
+            return error.InvalidLlvmTriple;
         }
 
-        const opt_level: llvm.CodeGenOptLevel = if (options.optimize_mode == .Debug) .None else .Aggressive;
+        const opt_level: llvm.CodeGenOptLevel = if (options.optimize_mode == .Debug)
+            .None
+        else
+            .Aggressive;
+
+        const reloc_mode: llvm.RelocMode = if (options.pic)
+            .PIC
+        else if (options.link_mode == .Dynamic)
+            llvm.RelocMode.DynamicNoPIC
+        else
+            .Static;
+
+        const code_model: llvm.CodeModel = switch (options.machine_code_model) {
+            .default => .Default,
+            .tiny => .Tiny,
+            .small => .Small,
+            .kernel => .Kernel,
+            .medium => .Medium,
+            .large => .Large,
+        };
+
+        // TODO handle float ABI better- it should depend on the ABI portion of std.Target
+        const float_abi: llvm.ABIType = .Default;
+
+        // TODO a way to override this as part of std.Target ABI?
+        const abi_name: ?[*:0]const u8 = switch (options.target.cpu.arch) {
+            .riscv32 => switch (options.target.os.tag) {
+                .linux => "ilp32d",
+                else => "ilp32",
+            },
+            .riscv64 => switch (options.target.os.tag) {
+                .linux => "lp64d",
+                else => "lp64",
+            },
+            else => null,
+        };
+
         const target_machine = llvm.TargetMachine.create(
             target,
             llvm_target_triple.ptr,
-            "",
-            "",
+            if (options.target.cpu.model.llvm_name) |s| s.ptr else null,
+            options.llvm_cpu_features,
             opt_level,
-            .Static,
-            .Default,
+            reloc_mode,
+            code_model,
+            options.function_sections,
+            float_abi,
+            abi_name,
         );
         errdefer target_machine.dispose();
 
-        self.* = .{
+        return Object{
             .llvm_module = llvm_module,
             .context = context,
             .target_machine = target_machine,
-            .object_pathZ = object_pathZ,
         };
-        return self;
     }
 
-    pub fn deinit(self: *Object, allocator: *Allocator) void {
+    pub fn deinit(self: *Object) void {
         self.target_machine.dispose();
         self.llvm_module.dispose();
         self.context.dispose();
+        self.* = undefined;
+    }
 
-        allocator.free(self.object_pathZ);
-        allocator.destroy(self);
+    pub fn destroy(self: *Object, gpa: *Allocator) void {
+        self.deinit();
+        gpa.destroy(self);
     }
 
     fn initializeLLVMTargets() void {
@@ -240,38 +263,81 @@ pub const Object = struct {
         llvm.initializeAllAsmParsers();
     }
 
+    fn locPath(
+        arena: *Allocator,
+        opt_loc: ?Compilation.EmitLoc,
+        cache_directory: Compilation.Directory,
+    ) !?[*:0]u8 {
+        const loc = opt_loc orelse return null;
+        const directory = loc.directory orelse cache_directory;
+        const slice = try directory.joinZ(arena, &[_][]const u8{loc.basename});
+        return slice.ptr;
+    }
+
     pub fn flushModule(self: *Object, comp: *Compilation) !void {
         if (comp.verbose_llvm_ir) {
-            const dump = self.llvm_module.printToString();
-            defer llvm.disposeMessage(dump);
-
-            const stderr = std.io.getStdErr().writer();
-            try stderr.writeAll(std.mem.spanZ(dump));
+            self.llvm_module.dump();
         }
 
-        {
+        if (std.debug.runtime_safety) {
             var error_message: [*:0]const u8 = undefined;
             // verifyModule always allocs the error_message even if there is no error
             defer llvm.disposeMessage(error_message);
 
             if (self.llvm_module.verify(.ReturnStatus, &error_message).toBool()) {
-                const stderr = std.io.getStdErr().writer();
-                try stderr.print("broken LLVM module found: {s}\nThis is a bug in the Zig compiler.", .{error_message});
-                return error.BrokenLLVMModule;
+                std.debug.print("\n{s}\n", .{error_message});
+                @panic("LLVM module verification failed");
             }
         }
 
+        var arena_allocator = std.heap.ArenaAllocator.init(comp.gpa);
+        defer arena_allocator.deinit();
+        const arena = &arena_allocator.allocator;
+
+        const mod = comp.bin_file.options.module.?;
+        const cache_dir = mod.zig_cache_artifact_directory;
+
+        const emit_bin_path: ?[*:0]const u8 = if (comp.bin_file.options.emit != null) blk: {
+            const obj_basename = try std.zig.binNameAlloc(arena, .{
+                .root_name = comp.bin_file.options.root_name,
+                .target = comp.bin_file.options.target,
+                .output_mode = .Obj,
+            });
+            if (cache_dir.joinZ(arena, &[_][]const u8{obj_basename})) |p| {
+                break :blk p.ptr;
+            } else |err| {
+                return err;
+            }
+        } else null;
+
+        const emit_asm_path = try locPath(arena, comp.emit_asm, cache_dir);
+        const emit_llvm_ir_path = try locPath(arena, comp.emit_llvm_ir, cache_dir);
+        const emit_llvm_bc_path = try locPath(arena, comp.emit_llvm_bc, cache_dir);
+
         var error_message: [*:0]const u8 = undefined;
         if (self.target_machine.emitToFile(
             self.llvm_module,
-            self.object_pathZ.ptr,
-            .ObjectFile,
             &error_message,
-        ).toBool()) {
+            comp.bin_file.options.optimize_mode == .Debug,
+            comp.bin_file.options.optimize_mode == .ReleaseSmall,
+            comp.time_report,
+            comp.bin_file.options.tsan,
+            comp.bin_file.options.lto,
+            emit_asm_path,
+            emit_bin_path,
+            emit_llvm_ir_path,
+            emit_llvm_bc_path,
+        )) {
             defer llvm.disposeMessage(error_message);
 
-            const stderr = std.io.getStdErr().writer();
-            try stderr.print("LLVM failed to emit file: {s}\n", .{error_message});
+            const emit_asm_msg = emit_asm_path orelse "(none)";
+            const emit_bin_msg = emit_bin_path orelse "(none)";
+            const emit_llvm_ir_msg = emit_llvm_ir_path orelse "(none)";
+            const emit_llvm_bc_msg = emit_llvm_bc_path orelse "(none)";
+            log.err("LLVM failed to emit asm={s} bin={s} ir={s} bc={s}: {s}", .{
+                emit_asm_msg,  emit_bin_msg, emit_llvm_ir_msg, emit_llvm_bc_msg,
+                error_message,
+            });
             return error.FailedToEmit;
         }
     }
src/link/Coff.zig
@@ -17,9 +17,9 @@ const link = @import("../link.zig");
 const build_options = @import("build_options");
 const Cache = @import("../Cache.zig");
 const mingw = @import("../mingw.zig");
-const llvm_backend = @import("../codegen/llvm.zig");
 const Air = @import("../Air.zig");
 const Liveness = @import("../Liveness.zig");
+const LlvmObject = @import("../codegen/llvm.zig").Object;
 
 const allocation_padding = 4 / 3;
 const minimum_text_block_size = 64 * allocation_padding;
@@ -37,7 +37,7 @@ pub const base_tag: link.File.Tag = .coff;
 const msdos_stub = @embedFile("msdos-stub.bin");
 
 /// If this is not null, an object file is created by LLVM and linked with LLD afterwards.
-llvm_object: ?*llvm_backend.Object = null,
+llvm_object: ?*LlvmObject = null,
 
 base: link.File,
 ptr_width: PtrWidth,
@@ -132,7 +132,7 @@ pub fn openPath(allocator: *Allocator, sub_path: []const u8, options: link.Optio
         const self = try createEmpty(allocator, options);
         errdefer self.base.destroy();
 
-        self.llvm_object = try llvm_backend.Object.create(allocator, sub_path, options);
+        self.llvm_object = try LlvmObject.create(allocator, options);
         return self;
     }
 
@@ -820,8 +820,11 @@ pub fn flushModule(self: *Coff, comp: *Compilation) !void {
     const tracy = trace(@src());
     defer tracy.end();
 
-    if (build_options.have_llvm)
-        if (self.llvm_object) |llvm_object| return try llvm_object.flushModule(comp);
+    if (build_options.have_llvm) {
+        if (self.llvm_object) |llvm_object| {
+            return try llvm_object.flushModule(comp);
+        }
+    }
 
     if (self.text_section_size_dirty) {
         // Write the new raw size in the .text header
@@ -1395,8 +1398,9 @@ pub fn updateDeclLineNumber(self: *Coff, module: *Module, decl: *Module.Decl) !v
 }
 
 pub fn deinit(self: *Coff) void {
-    if (build_options.have_llvm)
-        if (self.llvm_object) |ir_module| ir_module.deinit(self.base.allocator);
+    if (build_options.have_llvm) {
+        if (self.llvm_object) |llvm_object| llvm_object.destroy(self.base.allocator);
+    }
 
     self.text_block_free_list.deinit(self.base.allocator);
     self.offset_table.deinit(self.base.allocator);
src/link/Elf.zig
@@ -25,9 +25,9 @@ const target_util = @import("../target.zig");
 const glibc = @import("../glibc.zig");
 const musl = @import("../musl.zig");
 const Cache = @import("../Cache.zig");
-const llvm_backend = @import("../codegen/llvm.zig");
 const Air = @import("../Air.zig");
 const Liveness = @import("../Liveness.zig");
+const LlvmObject = @import("../codegen/llvm.zig").Object;
 
 const default_entry_addr = 0x8000000;
 
@@ -38,7 +38,7 @@ base: File,
 ptr_width: PtrWidth,
 
 /// If this is not null, an object file is created by LLVM and linked with LLD afterwards.
-llvm_object: ?*llvm_backend.Object = null,
+llvm_object: ?*LlvmObject = null,
 
 /// Stored in native-endian format, depending on target endianness needs to be bswapped on read/write.
 /// Same order as in the file.
@@ -235,7 +235,7 @@ pub fn openPath(allocator: *Allocator, sub_path: []const u8, options: link.Optio
         const self = try createEmpty(allocator, options);
         errdefer self.base.destroy();
 
-        self.llvm_object = try llvm_backend.Object.create(allocator, sub_path, options);
+        self.llvm_object = try LlvmObject.create(allocator, options);
         return self;
     }
 
@@ -301,9 +301,9 @@ pub fn createEmpty(gpa: *Allocator, options: link.Options) !*Elf {
 }
 
 pub fn deinit(self: *Elf) void {
-    if (build_options.have_llvm)
-        if (self.llvm_object) |ir_module|
-            ir_module.deinit(self.base.allocator);
+    if (build_options.have_llvm) {
+        if (self.llvm_object) |llvm_object| llvm_object.destroy(self.base.allocator);
+    }
 
     self.sections.deinit(self.base.allocator);
     self.program_headers.deinit(self.base.allocator);
@@ -750,8 +750,8 @@ pub fn flushModule(self: *Elf, comp: *Compilation) !void {
     if (build_options.have_llvm)
         if (self.llvm_object) |llvm_object| return try llvm_object.flushModule(comp);
 
-    // TODO This linker code currently assumes there is only 1 compilation unit and it corresponds to the
-    // Zig source code.
+    // TODO This linker code currently assumes there is only 1 compilation unit and it
+    // corresponds to the Zig source code.
     const module = self.base.options.module orelse return error.LinkingWithoutZigSourceUnimplemented;
 
     const target_endian = self.base.options.target.cpu.arch.endian();
src/link/MachO.zig
@@ -30,7 +30,7 @@ const DebugSymbols = @import("MachO/DebugSymbols.zig");
 const Trie = @import("MachO/Trie.zig");
 const CodeSignature = @import("MachO/CodeSignature.zig");
 const Zld = @import("MachO/Zld.zig");
-const llvm_backend = @import("../codegen/llvm.zig");
+const LlvmObject = @import("../codegen/llvm.zig").Object;
 
 usingnamespace @import("MachO/commands.zig");
 
@@ -39,7 +39,7 @@ pub const base_tag: File.Tag = File.Tag.macho;
 base: File,
 
 /// If this is not null, an object file is created by LLVM and linked with LLD afterwards.
-llvm_object: ?*llvm_backend.Object = null,
+llvm_object: ?*LlvmObject = null,
 
 /// Debug symbols bundle (or dSym).
 d_sym: ?DebugSymbols = null,
@@ -355,7 +355,7 @@ pub fn openPath(allocator: *Allocator, sub_path: []const u8, options: link.Optio
         const self = try createEmpty(allocator, options);
         errdefer self.base.destroy();
 
-        self.llvm_object = try llvm_backend.Object.create(allocator, sub_path, options);
+        self.llvm_object = try LlvmObject.create(allocator, options);
         return self;
     }
 
@@ -989,6 +989,9 @@ fn darwinArchString(arch: std.Target.Cpu.Arch) []const u8 {
 }
 
 pub fn deinit(self: *MachO) void {
+    if (build_options.have_llvm) {
+        if (self.llvm_object) |llvm_object| llvm_object.destroy(self.base.allocator);
+    }
     if (self.d_sym) |*ds| {
         ds.deinit(self.base.allocator);
     }
src/link/Wasm.zig
@@ -19,7 +19,7 @@ const build_options = @import("build_options");
 const wasi_libc = @import("../wasi_libc.zig");
 const Cache = @import("../Cache.zig");
 const TypedValue = @import("../TypedValue.zig");
-const llvm_backend = @import("../codegen/llvm.zig");
+const LlvmObject = @import("../codegen/llvm.zig").Object;
 const Air = @import("../Air.zig");
 const Liveness = @import("../Liveness.zig");
 
@@ -27,7 +27,7 @@ pub const base_tag = link.File.Tag.wasm;
 
 base: link.File,
 /// If this is not null, an object file is created by LLVM and linked with LLD afterwards.
-llvm_object: ?*llvm_backend.Object = null,
+llvm_object: ?*LlvmObject = null,
 /// List of all function Decls to be written to the output file. The index of
 /// each Decl in this list at the time of writing the binary is used as the
 /// function index. In the event where ext_funcs' size is not 0, the index of
@@ -121,7 +121,7 @@ pub fn openPath(allocator: *Allocator, sub_path: []const u8, options: link.Optio
         const self = try createEmpty(allocator, options);
         errdefer self.base.destroy();
 
-        self.llvm_object = try llvm_backend.Object.create(allocator, sub_path, options);
+        self.llvm_object = try LlvmObject.create(allocator, options);
         return self;
     }
 
@@ -153,6 +153,9 @@ pub fn createEmpty(gpa: *Allocator, options: link.Options) !*Wasm {
 }
 
 pub fn deinit(self: *Wasm) void {
+    if (build_options.have_llvm) {
+        if (self.llvm_object) |llvm_object| llvm_object.destroy(self.base.allocator);
+    }
     for (self.symbols.items) |decl| {
         decl.fn_link.wasm.functype.deinit(self.base.allocator);
         decl.fn_link.wasm.code.deinit(self.base.allocator);
src/stage1/all_types.hpp
@@ -2090,6 +2090,7 @@ struct CodeGen {
     Buf h_file_output_path;
     Buf asm_file_output_path;
     Buf llvm_ir_file_output_path;
+    Buf bitcode_file_output_path;
     Buf analysis_json_output_path;
     Buf docs_output_path;
 
src/stage1/codegen.cpp
@@ -8506,19 +8506,22 @@ static void zig_llvm_emit_output(CodeGen *g) {
     const char *asm_filename = nullptr;
     const char *bin_filename = nullptr;
     const char *llvm_ir_filename = nullptr;
+    const char *bitcode_filename = nullptr;
 
     if (buf_len(&g->o_file_output_path) != 0) bin_filename = buf_ptr(&g->o_file_output_path);
     if (buf_len(&g->asm_file_output_path) != 0) asm_filename = buf_ptr(&g->asm_file_output_path);
     if (buf_len(&g->llvm_ir_file_output_path) != 0) llvm_ir_filename = buf_ptr(&g->llvm_ir_file_output_path);
+    if (buf_len(&g->bitcode_file_output_path) != 0) bitcode_filename = buf_ptr(&g->bitcode_file_output_path);
 
-    // Unfortunately, LLVM shits the bed when we ask for both binary and assembly. So we call the entire
-    // pipeline multiple times if this is requested.
+    // Unfortunately, LLVM shits the bed when we ask for both binary and assembly.
+    // So we call the entire pipeline multiple times if this is requested.
     if (asm_filename != nullptr && bin_filename != nullptr) {
         if (ZigLLVMTargetMachineEmitToFile(g->target_machine, g->module, &err_msg,
             g->build_mode == BuildModeDebug, is_small, g->enable_time_report, g->tsan_enabled,
-            g->have_lto, nullptr, bin_filename, llvm_ir_filename))
+            g->have_lto, nullptr, bin_filename, llvm_ir_filename, nullptr))
         {
-            fprintf(stderr, "LLVM failed to emit file: %s\n", err_msg);
+            fprintf(stderr, "LLVM failed to emit bin=%s, ir=%s: %s\n",
+                    bin_filename, llvm_ir_filename, err_msg);
             exit(1);
         }
         bin_filename = nullptr;
@@ -8527,9 +8530,11 @@ static void zig_llvm_emit_output(CodeGen *g) {
 
     if (ZigLLVMTargetMachineEmitToFile(g->target_machine, g->module, &err_msg,
         g->build_mode == BuildModeDebug, is_small, g->enable_time_report, g->tsan_enabled,
-        g->have_lto, asm_filename, bin_filename, llvm_ir_filename))
+        g->have_lto, asm_filename, bin_filename, llvm_ir_filename, bitcode_filename))
     {
-        fprintf(stderr, "LLVM failed to emit file: %s\n", err_msg);
+        fprintf(stderr, "LLVM failed to emit asm=%s, bin=%s, ir=%s, bc=%s: %s\n",
+                asm_filename, bin_filename, llvm_ir_filename, bitcode_filename,
+                err_msg);
         exit(1);
     }
 
src/stage1/stage1.cpp
@@ -73,6 +73,7 @@ void zig_stage1_build_object(struct ZigStage1 *stage1) {
     buf_init_from_mem(&g->h_file_output_path, stage1->emit_h_ptr, stage1->emit_h_len);
     buf_init_from_mem(&g->asm_file_output_path, stage1->emit_asm_ptr, stage1->emit_asm_len);
     buf_init_from_mem(&g->llvm_ir_file_output_path, stage1->emit_llvm_ir_ptr, stage1->emit_llvm_ir_len);
+    buf_init_from_mem(&g->bitcode_file_output_path, stage1->emit_bitcode_ptr, stage1->emit_bitcode_len);
     buf_init_from_mem(&g->analysis_json_output_path, stage1->emit_analysis_json_ptr, stage1->emit_analysis_json_len);
     buf_init_from_mem(&g->docs_output_path, stage1->emit_docs_ptr, stage1->emit_docs_len);
 
src/stage1/stage1.h
@@ -157,6 +157,9 @@ struct ZigStage1 {
     const char *emit_llvm_ir_ptr;
     size_t emit_llvm_ir_len;
 
+    const char *emit_bitcode_ptr;
+    size_t emit_bitcode_len;
+
     const char *emit_analysis_json_ptr;
     size_t emit_analysis_json_len;
 
src/Compilation.zig
@@ -143,6 +143,7 @@ debug_compiler_runtime_libs: bool,
 
 emit_asm: ?EmitLoc,
 emit_llvm_ir: ?EmitLoc,
+emit_llvm_bc: ?EmitLoc,
 emit_analysis: ?EmitLoc,
 emit_docs: ?EmitLoc,
 
@@ -586,6 +587,17 @@ pub const Directory = struct {
             return std.fs.path.join(allocator, paths);
         }
     }
+
+    pub fn joinZ(self: Directory, allocator: *Allocator, paths: []const []const u8) ![:0]u8 {
+        if (self.path) |p| {
+            // TODO clean way to do this with only 1 allocation
+            const part2 = try std.fs.path.join(allocator, paths);
+            defer allocator.free(part2);
+            return std.fs.path.joinZ(allocator, &[_][]const u8{ p, part2 });
+        } else {
+            return std.fs.path.joinZ(allocator, paths);
+        }
+    }
 };
 
 pub const EmitLoc = struct {
@@ -623,6 +635,8 @@ pub const InitOptions = struct {
     emit_asm: ?EmitLoc = null,
     /// `null` means to not emit LLVM IR.
     emit_llvm_ir: ?EmitLoc = null,
+    /// `null` means to not emit LLVM module bitcode.
+    emit_llvm_bc: ?EmitLoc = null,
     /// `null` means to not emit semantic analysis JSON.
     emit_analysis: ?EmitLoc = null,
     /// `null` means to not emit docs.
@@ -819,6 +833,10 @@ pub fn create(gpa: *Allocator, options: InitOptions) !*Compilation {
                     break :blk false;
                 }
             }
+            // If we have no zig code to compile, no need for stage1 backend.
+            if (options.root_pkg == null)
+                break :blk false;
+
             break :blk build_options.is_stage1;
         };
 
@@ -835,6 +853,10 @@ pub fn create(gpa: *Allocator, options: InitOptions) !*Compilation {
             if (ofmt == .c)
                 break :blk false;
 
+            // If emitting to LLVM bitcode object format, must use LLVM backend.
+            if (options.emit_llvm_ir != null or options.emit_llvm_bc != null)
+                break :blk true;
+
             // The stage1 compiler depends on the stage1 C++ LLVM backend
             // to compile zig code.
             if (use_stage1)
@@ -853,6 +875,12 @@ pub fn create(gpa: *Allocator, options: InitOptions) !*Compilation {
             if (options.machine_code_model != .default) {
                 return error.MachineCodeModelNotSupportedWithoutLlvm;
             }
+            if (options.emit_llvm_ir != null or options.emit_llvm_bc != null) {
+                return error.EmittingLlvmModuleRequiresUsingLlvmBackend;
+            }
+            if (use_stage1) {
+                return error.@"stage1 only supports LLVM backend";
+            }
         }
 
         const tsan = options.want_tsan orelse false;
@@ -1381,6 +1409,7 @@ pub fn create(gpa: *Allocator, options: InitOptions) !*Compilation {
             .bin_file = bin_file,
             .emit_asm = options.emit_asm,
             .emit_llvm_ir = options.emit_llvm_ir,
+            .emit_llvm_bc = options.emit_llvm_bc,
             .emit_analysis = options.emit_analysis,
             .emit_docs = options.emit_docs,
             .work_queue = std.fifo.LinearFifo(Job, .Dynamic).init(gpa),
@@ -2728,7 +2757,10 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: *std.P
         comp.bin_file.options.root_name
     else
         c_source_basename[0 .. c_source_basename.len - std.fs.path.extension(c_source_basename).len];
-    const o_basename = try std.fmt.allocPrint(arena, "{s}{s}", .{ o_basename_noext, comp.getTarget().oFileExt() });
+    const o_basename = try std.fmt.allocPrint(arena, "{s}{s}", .{
+        o_basename_noext,
+        comp.bin_file.options.object_format.fileExt(comp.bin_file.options.target.cpu.arch),
+    });
 
     const digest = if (!comp.disable_c_depfile and try man.hit()) man.final() else blk: {
         var argv = std.ArrayList([]const u8).init(comp.gpa);
@@ -3978,6 +4010,7 @@ fn updateStage1Module(comp: *Compilation, main_progress_node: *std.Progress.Node
     }
     man.hash.addOptionalEmitLoc(comp.emit_asm);
     man.hash.addOptionalEmitLoc(comp.emit_llvm_ir);
+    man.hash.addOptionalEmitLoc(comp.emit_llvm_bc);
     man.hash.addOptionalEmitLoc(comp.emit_analysis);
     man.hash.addOptionalEmitLoc(comp.emit_docs);
     man.hash.add(comp.test_evented_io);
@@ -4083,13 +4116,14 @@ fn updateStage1Module(comp: *Compilation, main_progress_node: *std.Progress.Node
     ) orelse return error.OutOfMemory;
 
     const emit_bin_path = if (comp.bin_file.options.emit != null) blk: {
-        const bin_basename = try std.zig.binNameAlloc(arena, .{
+        const obj_basename = try std.zig.binNameAlloc(arena, .{
             .root_name = comp.bin_file.options.root_name,
             .target = target,
             .output_mode = .Obj,
         });
-        break :blk try directory.join(arena, &[_][]const u8{bin_basename});
+        break :blk try directory.join(arena, &[_][]const u8{obj_basename});
     } else "";
+
     if (mod.emit_h != null) {
         log.warn("-femit-h is not available in the stage1 backend; no .h file will be produced", .{});
     }
@@ -4097,6 +4131,7 @@ fn updateStage1Module(comp: *Compilation, main_progress_node: *std.Progress.Node
     const emit_h_path = try stage1LocPath(arena, emit_h_loc, directory);
     const emit_asm_path = try stage1LocPath(arena, comp.emit_asm, directory);
     const emit_llvm_ir_path = try stage1LocPath(arena, comp.emit_llvm_ir, directory);
+    const emit_llvm_bc_path = try stage1LocPath(arena, comp.emit_llvm_bc, directory);
     const emit_analysis_path = try stage1LocPath(arena, comp.emit_analysis, directory);
     const emit_docs_path = try stage1LocPath(arena, comp.emit_docs, directory);
     const stage1_pkg = try createStage1Pkg(arena, "root", mod.root_pkg, null);
@@ -4117,6 +4152,8 @@ fn updateStage1Module(comp: *Compilation, main_progress_node: *std.Progress.Node
         .emit_asm_len = emit_asm_path.len,
         .emit_llvm_ir_ptr = emit_llvm_ir_path.ptr,
         .emit_llvm_ir_len = emit_llvm_ir_path.len,
+        .emit_bitcode_ptr = emit_llvm_bc_path.ptr,
+        .emit_bitcode_len = emit_llvm_bc_path.len,
         .emit_analysis_json_ptr = emit_analysis_path.ptr,
         .emit_analysis_json_len = emit_analysis_path.len,
         .emit_docs_ptr = emit_docs_path.ptr,
src/link.zig
@@ -206,7 +206,8 @@ pub const File = struct {
         const use_lld = build_options.have_llvm and options.use_lld; // comptime known false when !have_llvm
         const sub_path = if (use_lld) blk: {
             if (options.module == null) {
-                // No point in opening a file, we would not write anything to it. Initialize with empty.
+                // No point in opening a file, we would not write anything to it.
+                // Initialize with empty.
                 return switch (options.object_format) {
                     .coff => &(try Coff.createEmpty(allocator, options)).base,
                     .elf => &(try Elf.createEmpty(allocator, options)).base,
@@ -219,8 +220,11 @@ pub const File = struct {
                     .raw => return error.RawObjectFormatUnimplemented,
                 };
             }
-            // Open a temporary object file, not the final output file because we want to link with LLD.
-            break :blk try std.fmt.allocPrint(allocator, "{s}{s}", .{ emit.sub_path, options.target.oFileExt() });
+            // Open a temporary object file, not the final output file because we
+            // want to link with LLD.
+            break :blk try std.fmt.allocPrint(allocator, "{s}{s}", .{
+                emit.sub_path, options.object_format.fileExt(options.target.cpu.arch),
+            });
         } else emit.sub_path;
         errdefer if (use_lld) allocator.free(sub_path);
 
src/main.zig
@@ -301,6 +301,8 @@ const usage_build_generic =
     \\  -fno-emit-asm             (default) Do not output .s (assembly code)
     \\  -femit-llvm-ir[=path]     Produce a .ll file with LLVM IR (requires LLVM extensions)
     \\  -fno-emit-llvm-ir         (default) Do not produce a .ll file with LLVM IR
+    \\  -femit-llvm-bc[=path]     Produce a LLVM module as a .bc file (requires LLVM extensions)
+    \\  -fno-emit-llvm-bc         (default) Do not produce a LLVM module as a .bc file
     \\  -femit-h[=path]           Generate a C header file (.h)
     \\  -fno-emit-h               (default) Do not generate a C header file (.h)
     \\  -femit-docs[=path]        Create a docs/ dir with html documentation
@@ -359,7 +361,7 @@ const usage_build_generic =
     \\  --single-threaded         Code assumes it is only used single-threaded
     \\  -ofmt=[mode]              Override target object format
     \\    elf                     Executable and Linking Format
-    \\    c                       Compile to C source code
+    \\    c                       C source code
     \\    wasm                    WebAssembly
     \\    coff                    Common Object File Format (Windows)
     \\    macho                   macOS relocatables
@@ -551,6 +553,7 @@ fn buildOutputType(
     var emit_bin: EmitBin = .yes_default_path;
     var emit_asm: Emit = .no;
     var emit_llvm_ir: Emit = .no;
+    var emit_llvm_bc: Emit = .no;
     var emit_docs: Emit = .no;
     var emit_analysis: Emit = .no;
     var target_arch_os_abi: []const u8 = "native";
@@ -1010,6 +1013,12 @@ fn buildOutputType(
                         emit_llvm_ir = .{ .yes = arg["-femit-llvm-ir=".len..] };
                     } else if (mem.eql(u8, arg, "-fno-emit-llvm-ir")) {
                         emit_llvm_ir = .no;
+                    } else if (mem.eql(u8, arg, "-femit-llvm-bc")) {
+                        emit_llvm_bc = .yes_default_path;
+                    } else if (mem.startsWith(u8, arg, "-femit-llvm-bc=")) {
+                        emit_llvm_bc = .{ .yes = arg["-femit-llvm-bc=".len..] };
+                    } else if (mem.eql(u8, arg, "-fno-emit-llvm-bc")) {
+                        emit_llvm_bc = .no;
                     } else if (mem.eql(u8, arg, "-femit-docs")) {
                         emit_docs = .yes_default_path;
                     } else if (mem.startsWith(u8, arg, "-femit-docs=")) {
@@ -1815,10 +1824,10 @@ fn buildOutputType(
     var emit_h_resolved = emit_h.resolve(default_h_basename) catch |err| {
         switch (emit_h) {
             .yes => {
-                fatal("unable to open directory from argument 'femit-h', '{s}': {s}", .{ emit_h.yes, @errorName(err) });
+                fatal("unable to open directory from argument '-femit-h', '{s}': {s}", .{ emit_h.yes, @errorName(err) });
             },
             .yes_default_path => {
-                fatal("unable to open directory from arguments 'name' or 'soname', '{s}': {s}", .{ default_h_basename, @errorName(err) });
+                fatal("unable to open directory from arguments '--name' or '-fsoname', '{s}': {s}", .{ default_h_basename, @errorName(err) });
             },
             .no => unreachable,
         }
@@ -1829,10 +1838,10 @@ fn buildOutputType(
     var emit_asm_resolved = emit_asm.resolve(default_asm_basename) catch |err| {
         switch (emit_asm) {
             .yes => {
-                fatal("unable to open directory from argument 'femit-asm', '{s}': {s}", .{ emit_asm.yes, @errorName(err) });
+                fatal("unable to open directory from argument '-femit-asm', '{s}': {s}", .{ emit_asm.yes, @errorName(err) });
             },
             .yes_default_path => {
-                fatal("unable to open directory from arguments 'name' or 'soname', '{s}': {s}", .{ default_asm_basename, @errorName(err) });
+                fatal("unable to open directory from arguments '--name' or '-fsoname', '{s}': {s}", .{ default_asm_basename, @errorName(err) });
             },
             .no => unreachable,
         }
@@ -1843,16 +1852,30 @@ fn buildOutputType(
     var emit_llvm_ir_resolved = emit_llvm_ir.resolve(default_llvm_ir_basename) catch |err| {
         switch (emit_llvm_ir) {
             .yes => {
-                fatal("unable to open directory from argument 'femit-llvm-ir', '{s}': {s}", .{ emit_llvm_ir.yes, @errorName(err) });
+                fatal("unable to open directory from argument '-femit-llvm-ir', '{s}': {s}", .{ emit_llvm_ir.yes, @errorName(err) });
             },
             .yes_default_path => {
-                fatal("unable to open directory from arguments 'name' or 'soname', '{s}': {s}", .{ default_llvm_ir_basename, @errorName(err) });
+                fatal("unable to open directory from arguments '--name' or '-fsoname', '{s}': {s}", .{ default_llvm_ir_basename, @errorName(err) });
             },
             .no => unreachable,
         }
     };
     defer emit_llvm_ir_resolved.deinit();
 
+    const default_llvm_bc_basename = try std.fmt.allocPrint(arena, "{s}.bc", .{root_name});
+    var emit_llvm_bc_resolved = emit_llvm_bc.resolve(default_llvm_bc_basename) catch |err| {
+        switch (emit_llvm_bc) {
+            .yes => {
+                fatal("unable to open directory from argument '-femit-llvm-bc', '{s}': {s}", .{ emit_llvm_bc.yes, @errorName(err) });
+            },
+            .yes_default_path => {
+                fatal("unable to open directory from arguments '--name' or '-fsoname', '{s}': {s}", .{ default_llvm_bc_basename, @errorName(err) });
+            },
+            .no => unreachable,
+        }
+    };
+    defer emit_llvm_bc_resolved.deinit();
+
     const default_analysis_basename = try std.fmt.allocPrint(arena, "{s}-analysis.json", .{root_name});
     var emit_analysis_resolved = emit_analysis.resolve(default_analysis_basename) catch |err| {
         switch (emit_analysis) {
@@ -1988,6 +2011,7 @@ fn buildOutputType(
         .emit_h = emit_h_resolved.data,
         .emit_asm = emit_asm_resolved.data,
         .emit_llvm_ir = emit_llvm_ir_resolved.data,
+        .emit_llvm_bc = emit_llvm_bc_resolved.data,
         .emit_docs = emit_docs_resolved.data,
         .emit_analysis = emit_analysis_resolved.data,
         .link_mode = link_mode,
src/stage1.zig
@@ -95,6 +95,8 @@ pub const Module = extern struct {
     emit_asm_len: usize,
     emit_llvm_ir_ptr: [*]const u8,
     emit_llvm_ir_len: usize,
+    emit_bitcode_ptr: [*]const u8,
+    emit_bitcode_len: usize,
     emit_analysis_json_ptr: [*]const u8,
     emit_analysis_json_len: usize,
     emit_docs_ptr: [*]const u8,
src/zig_llvm.cpp
@@ -229,12 +229,14 @@ struct TimeTracerRAII {
 bool ZigLLVMTargetMachineEmitToFile(LLVMTargetMachineRef targ_machine_ref, LLVMModuleRef module_ref,
         char **error_message, bool is_debug,
         bool is_small, bool time_report, bool tsan, bool lto,
-        const char *asm_filename, const char *bin_filename, const char *llvm_ir_filename)
+        const char *asm_filename, const char *bin_filename,
+        const char *llvm_ir_filename, const char *bitcode_filename)
 {
     TimePassesIsEnabled = time_report;
 
     raw_fd_ostream *dest_asm_ptr = nullptr;
     raw_fd_ostream *dest_bin_ptr = nullptr;
+    raw_fd_ostream *dest_bitcode_ptr = nullptr;
 
     if (asm_filename) {
         std::error_code EC;
@@ -252,9 +254,19 @@ bool ZigLLVMTargetMachineEmitToFile(LLVMTargetMachineRef targ_machine_ref, LLVMM
             return true;
         }
     }
+    if (bitcode_filename) {
+        std::error_code EC;
+        dest_bitcode_ptr = new(std::nothrow) raw_fd_ostream(bitcode_filename, EC, sys::fs::F_None);
+        if (EC) {
+            *error_message = strdup((const char *)StringRef(EC.message()).bytes_begin());
+            return true;
+        }
+    }
 
     std::unique_ptr<raw_fd_ostream> dest_asm(dest_asm_ptr),
-                                    dest_bin(dest_bin_ptr);
+                                    dest_bin(dest_bin_ptr),
+                                    dest_bitcode(dest_bitcode_ptr);
+
 
     auto PID = sys::Process::getProcessId();
     std::string ProcName = "zig-";
@@ -389,6 +401,9 @@ bool ZigLLVMTargetMachineEmitToFile(LLVMTargetMachineRef targ_machine_ref, LLVMM
     if (dest_bin && lto) {
         WriteBitcodeToFile(module, *dest_bin);
     }
+    if (dest_bitcode) {
+        WriteBitcodeToFile(module, *dest_bitcode);
+    }
 
     if (time_report) {
         TimerGroup::printAll(errs());
src/zig_llvm.h
@@ -49,7 +49,8 @@ ZIG_EXTERN_C char *ZigLLVMGetNativeFeatures(void);
 ZIG_EXTERN_C bool ZigLLVMTargetMachineEmitToFile(LLVMTargetMachineRef targ_machine_ref, LLVMModuleRef module_ref,
         char **error_message, bool is_debug,
         bool is_small, bool time_report, bool tsan, bool lto,
-        const char *asm_filename, const char *bin_filename, const char *llvm_ir_filename);
+        const char *asm_filename, const char *bin_filename,
+        const char *llvm_ir_filename, const char *bitcode_filename);
 
 
 enum ZigLLVMABIType {