Commit 10f4486c0b

Jacob Young <jacobly0@users.noreply.github.com>
2023-11-09 22:41:04
Compilation: forward clang diagnostics to error bundles
1 parent 6b9f7e2
Changed files (2)
src/codegen/llvm/BitcodeReader.zig
@@ -0,0 +1,515 @@
+allocator: std.mem.Allocator,
+record_arena: std.heap.ArenaAllocator.State,
+reader: std.io.AnyReader,
+keep_names: bool,
+bit_buffer: u32,
+bit_offset: u5,
+stack: std.ArrayListUnmanaged(State),
+block_info: std.AutoHashMapUnmanaged(u32, Block.Info),
+
+pub const Item = union(enum) {
+    start_block: Block,
+    record: Record,
+    end_block: Block,
+};
+
+pub const Block = struct {
+    name: []const u8,
+    id: u32,
+    len: u32,
+
+    const block_info: u32 = 0;
+    const first_reserved: u32 = 1;
+    const last_standard: u32 = 7;
+
+    const Info = struct {
+        block_name: []const u8,
+        record_names: std.AutoHashMapUnmanaged(u32, []const u8),
+        abbrevs: Abbrev.Store,
+
+        const default: Info = .{
+            .block_name = &.{},
+            .record_names = .{},
+            .abbrevs = .{ .abbrevs = .{} },
+        };
+
+        const set_bid: u32 = 1;
+        const block_name: u32 = 2;
+        const set_record_name: u32 = 3;
+
+        fn deinit(info: *Info, allocator: std.mem.Allocator) void {
+            allocator.free(info.block_name);
+            var record_names_it = info.record_names.valueIterator();
+            while (record_names_it.next()) |record_name| allocator.free(record_name.*);
+            info.record_names.deinit(allocator);
+            info.abbrevs.deinit(allocator);
+            info.* = undefined;
+        }
+    };
+};
+
+pub const Record = struct {
+    name: []const u8,
+    id: u32,
+    operands: []u64,
+    blob: []u8,
+
+    fn toOwnedAbbrev(record: Record, allocator: std.mem.Allocator) !Abbrev {
+        var operands = std.ArrayList(Abbrev.Operand).init(allocator);
+        defer operands.deinit();
+
+        assert(record.id == Abbrev.Builtin.define_abbrev.toRecordId());
+        var i: usize = 0;
+        while (i < record.operands.len) switch (record.operands[i]) {
+            Abbrev.Operand.literal => {
+                try operands.append(.{ .literal = record.operands[i + 1] });
+                i += 2;
+            },
+            @intFromEnum(Abbrev.Operand.Encoding.fixed) => {
+                try operands.append(.{ .encoding = .{ .fixed = @intCast(record.operands[i + 1]) } });
+                i += 2;
+            },
+            @intFromEnum(Abbrev.Operand.Encoding.vbr) => {
+                try operands.append(.{ .encoding = .{ .vbr = @intCast(record.operands[i + 1]) } });
+                i += 2;
+            },
+            @intFromEnum(Abbrev.Operand.Encoding.array) => {
+                try operands.append(.{ .encoding = .{ .array = 6 } });
+                i += 1;
+            },
+            @intFromEnum(Abbrev.Operand.Encoding.char6) => {
+                try operands.append(.{ .encoding = .char6 });
+                i += 1;
+            },
+            @intFromEnum(Abbrev.Operand.Encoding.blob) => {
+                try operands.append(.{ .encoding = .{ .blob = 6 } });
+                i += 1;
+            },
+            else => unreachable,
+        };
+
+        return .{ .operands = try operands.toOwnedSlice() };
+    }
+};
+
+pub const InitOptions = struct {
+    reader: std.io.AnyReader,
+    keep_names: bool = false,
+};
+pub fn init(allocator: std.mem.Allocator, options: InitOptions) BitcodeReader {
+    return .{
+        .allocator = allocator,
+        .record_arena = .{},
+        .reader = options.reader,
+        .keep_names = options.keep_names,
+        .bit_buffer = 0,
+        .bit_offset = 0,
+        .stack = .{},
+        .block_info = .{},
+    };
+}
+
+pub fn deinit(bc: *BitcodeReader) void {
+    var block_info_it = bc.block_info.valueIterator();
+    while (block_info_it.next()) |block_info| block_info.deinit(bc.allocator);
+    bc.block_info.deinit(bc.allocator);
+    for (bc.stack.items) |*state| state.deinit(bc.allocator);
+    bc.stack.deinit(bc.allocator);
+    bc.record_arena.promote(bc.allocator).deinit();
+    bc.* = undefined;
+}
+
+pub fn checkMagic(bc: *BitcodeReader, magic: *const [4]u8) !void {
+    var buffer: [4]u8 = undefined;
+    try bc.readBytes(&buffer);
+    if (!std.mem.eql(u8, &buffer, magic)) return error.InvalidMagic;
+
+    try bc.startBlock(null, 2);
+    try bc.block_info.put(bc.allocator, Block.block_info, Block.Info.default);
+}
+
+pub fn next(bc: *BitcodeReader) !?Item {
+    while (true) {
+        const record = (try bc.nextRecord()) orelse
+            return if (bc.stack.items.len > 1) error.EndOfStream else null;
+        switch (record.id) {
+            else => return .{ .record = record },
+            Abbrev.Builtin.end_block.toRecordId() => {
+                const block_id = bc.stack.items[bc.stack.items.len - 1].block_id.?;
+                try bc.endBlock();
+                return .{ .end_block = .{
+                    .name = if (bc.block_info.get(block_id)) |block_info|
+                        block_info.block_name
+                    else
+                        &.{},
+                    .id = block_id,
+                    .len = 0,
+                } };
+            },
+            Abbrev.Builtin.enter_subblock.toRecordId() => {
+                const block_id: u32 = @intCast(record.operands[0]);
+                switch (block_id) {
+                    Block.block_info => try bc.parseBlockInfoBlock(),
+                    Block.first_reserved...Block.last_standard => return error.UnsupportedBlockId,
+                    else => {
+                        try bc.startBlock(block_id, @intCast(record.operands[1]));
+                        return .{ .start_block = .{
+                            .name = if (bc.block_info.get(block_id)) |block_info|
+                                block_info.block_name
+                            else
+                                &.{},
+                            .id = block_id,
+                            .len = @intCast(record.operands[2]),
+                        } };
+                    },
+                }
+            },
+            Abbrev.Builtin.define_abbrev.toRecordId() => try bc.stack.items[bc.stack.items.len - 1]
+                .abbrevs.addOwnedAbbrev(bc.allocator, try record.toOwnedAbbrev(bc.allocator)),
+        }
+    }
+}
+
+pub fn skipBlock(bc: *BitcodeReader, block: Block) !void {
+    assert(bc.bit_offset == 0);
+    try bc.reader.skipBytes(@as(u34, block.len) * 4, .{});
+    try bc.endBlock();
+}
+
+fn nextRecord(bc: *BitcodeReader) !?Record {
+    const state = &bc.stack.items[bc.stack.items.len - 1];
+    const abbrev_id = bc.readFixed(u32, state.abbrev_id_width) catch |err| switch (err) {
+        error.EndOfStream => return null,
+        else => |e| return e,
+    };
+    if (abbrev_id >= state.abbrevs.abbrevs.items.len) return error.InvalidAbbrevId;
+    const abbrev = state.abbrevs.abbrevs.items[abbrev_id];
+
+    var record_arena = bc.record_arena.promote(bc.allocator);
+    defer bc.record_arena = record_arena.state;
+    _ = record_arena.reset(.retain_capacity);
+
+    var operands = try std.ArrayList(u64).initCapacity(record_arena.allocator(), abbrev.operands.len);
+    var blob = std.ArrayList(u8).init(record_arena.allocator());
+    for (abbrev.operands, 0..) |abbrev_operand, abbrev_operand_i| switch (abbrev_operand) {
+        .literal => |value| operands.appendAssumeCapacity(value),
+        .encoding => |abbrev_encoding| switch (abbrev_encoding) {
+            .fixed => |width| operands.appendAssumeCapacity(try bc.readFixed(u64, width)),
+            .vbr => |width| operands.appendAssumeCapacity(try bc.readVbr(u64, width)),
+            .array => |len_width| {
+                assert(abbrev_operand_i + 2 == abbrev.operands.len);
+                const len: usize = @intCast(try bc.readVbr(u32, len_width));
+                try operands.ensureUnusedCapacity(len);
+                for (0..len) |_| switch (abbrev.operands[abbrev.operands.len - 1]) {
+                    .literal => |elem_value| operands.appendAssumeCapacity(elem_value),
+                    .encoding => |elem_encoding| switch (elem_encoding) {
+                        .fixed => |elem_width| operands.appendAssumeCapacity(try bc.readFixed(u64, elem_width)),
+                        .vbr => |elem_width| operands.appendAssumeCapacity(try bc.readVbr(u64, elem_width)),
+                        .array, .blob => return error.InvalidArrayElement,
+                        .char6 => operands.appendAssumeCapacity(try bc.readChar6()),
+                    },
+                    .align_32_bits, .block_len => return error.UnsupportedArrayElement,
+                    .abbrev_op => switch (try bc.readFixed(u1, 1)) {
+                        1 => try operands.appendSlice(&.{
+                            Abbrev.Operand.literal,
+                            try bc.readVbr(u64, 8),
+                        }),
+                        0 => {
+                            const encoding: Abbrev.Operand.Encoding =
+                                @enumFromInt(try bc.readFixed(u3, 3));
+                            try operands.append(@intFromEnum(encoding));
+                            switch (encoding) {
+                                .fixed, .vbr => try operands.append(try bc.readVbr(u7, 5)),
+                                .array, .char6, .blob => {},
+                                _ => return error.UnsuportedAbbrevEncoding,
+                            }
+                        },
+                    },
+                };
+                break;
+            },
+            .char6 => operands.appendAssumeCapacity(try bc.readChar6()),
+            .blob => |len_width| {
+                assert(abbrev_operand_i + 1 == abbrev.operands.len);
+                const len = std.math.cast(usize, try bc.readVbr(u32, len_width)) orelse
+                    return error.Overflow;
+                bc.align32Bits();
+                try bc.readBytes(try blob.addManyAsSlice(len));
+                bc.align32Bits();
+            },
+        },
+        .align_32_bits => bc.align32Bits(),
+        .block_len => operands.appendAssumeCapacity(try bc.read32Bits()),
+        .abbrev_op => unreachable,
+    };
+    return .{
+        .name = name: {
+            if (operands.items.len < 1) break :name &.{};
+            const record_id = std.math.cast(u32, operands.items[0]) orelse break :name &.{};
+            if (state.block_id) |block_id| {
+                if (bc.block_info.get(block_id)) |block_info| {
+                    break :name block_info.record_names.get(record_id) orelse break :name &.{};
+                }
+            }
+            break :name &.{};
+        },
+        .id = std.math.cast(u32, operands.items[0]) orelse return error.InvalidRecordId,
+        .operands = operands.items[1..],
+        .blob = blob.items,
+    };
+}
+
+fn startBlock(bc: *BitcodeReader, block_id: ?u32, new_abbrev_len: u6) !void {
+    const abbrevs = if (block_id) |id|
+        if (bc.block_info.get(id)) |block_info| block_info.abbrevs.abbrevs.items else &.{}
+    else
+        &.{};
+
+    const state = try bc.stack.addOne(bc.allocator);
+    state.* = .{
+        .block_id = block_id,
+        .abbrev_id_width = new_abbrev_len,
+        .abbrevs = .{ .abbrevs = .{} },
+    };
+    try state.abbrevs.abbrevs.ensureTotalCapacity(
+        bc.allocator,
+        @typeInfo(Abbrev.Builtin).Enum.fields.len + abbrevs.len,
+    );
+
+    assert(state.abbrevs.abbrevs.items.len == @intFromEnum(Abbrev.Builtin.end_block));
+    try state.abbrevs.addAbbrevAssumeCapacity(bc.allocator, .{
+        .operands = &.{
+            .{ .literal = Abbrev.Builtin.end_block.toRecordId() },
+            .align_32_bits,
+        },
+    });
+    assert(state.abbrevs.abbrevs.items.len == @intFromEnum(Abbrev.Builtin.enter_subblock));
+    try state.abbrevs.addAbbrevAssumeCapacity(bc.allocator, .{
+        .operands = &.{
+            .{ .literal = Abbrev.Builtin.enter_subblock.toRecordId() },
+            .{ .encoding = .{ .vbr = 8 } }, // blockid
+            .{ .encoding = .{ .vbr = 4 } }, // newabbrevlen
+            .align_32_bits,
+            .block_len,
+        },
+    });
+    assert(state.abbrevs.abbrevs.items.len == @intFromEnum(Abbrev.Builtin.define_abbrev));
+    try state.abbrevs.addAbbrevAssumeCapacity(bc.allocator, .{
+        .operands = &.{
+            .{ .literal = Abbrev.Builtin.define_abbrev.toRecordId() },
+            .{ .encoding = .{ .array = 5 } }, // numabbrevops
+            .abbrev_op,
+        },
+    });
+    assert(state.abbrevs.abbrevs.items.len == @intFromEnum(Abbrev.Builtin.unabbrev_record));
+    try state.abbrevs.addAbbrevAssumeCapacity(bc.allocator, .{
+        .operands = &.{
+            .{ .encoding = .{ .vbr = 6 } }, // code
+            .{ .encoding = .{ .array = 6 } }, // numops
+            .{ .encoding = .{ .vbr = 6 } }, // ops
+        },
+    });
+    assert(state.abbrevs.abbrevs.items.len == @typeInfo(Abbrev.Builtin).Enum.fields.len);
+    for (abbrevs) |abbrev| try state.abbrevs.addAbbrevAssumeCapacity(bc.allocator, abbrev);
+}
+
+fn endBlock(bc: *BitcodeReader) !void {
+    if (bc.stack.items.len == 0) return error.InvalidEndBlock;
+    bc.stack.items[bc.stack.items.len - 1].deinit(bc.allocator);
+    bc.stack.items.len -= 1;
+}
+
+fn parseBlockInfoBlock(bc: *BitcodeReader) !void {
+    var block_id: ?u32 = null;
+    while (true) {
+        const record = (try bc.nextRecord()) orelse return error.EndOfStream;
+        switch (record.id) {
+            Abbrev.Builtin.end_block.toRecordId() => break,
+            Abbrev.Builtin.define_abbrev.toRecordId() => {
+                const gop = try bc.block_info.getOrPut(bc.allocator, block_id orelse
+                    return error.UnspecifiedBlockId);
+                if (!gop.found_existing) gop.value_ptr.* = Block.Info.default;
+                try gop.value_ptr.abbrevs.addOwnedAbbrev(
+                    bc.allocator,
+                    try record.toOwnedAbbrev(bc.allocator),
+                );
+            },
+            Block.Info.set_bid => block_id = std.math.cast(u32, record.operands[0]) orelse
+                return error.Overflow,
+            Block.Info.block_name => if (bc.keep_names) {
+                const gop = try bc.block_info.getOrPut(bc.allocator, block_id orelse
+                    return error.UnspecifiedBlockId);
+                if (!gop.found_existing) gop.value_ptr.* = Block.Info.default;
+                const name = try bc.allocator.alloc(u8, record.operands.len);
+                errdefer bc.allocator.free(name);
+                for (name, record.operands) |*byte, operand|
+                    byte.* = std.math.cast(u8, operand) orelse return error.InvalidName;
+                gop.value_ptr.block_name = name;
+            },
+            Block.Info.set_record_name => if (bc.keep_names) {
+                const gop = try bc.block_info.getOrPut(bc.allocator, block_id orelse
+                    return error.UnspecifiedBlockId);
+                if (!gop.found_existing) gop.value_ptr.* = Block.Info.default;
+                const name = try bc.allocator.alloc(u8, record.operands.len - 1);
+                errdefer bc.allocator.free(name);
+                for (name, record.operands[1..]) |*byte, operand|
+                    byte.* = std.math.cast(u8, operand) orelse return error.InvalidName;
+                try gop.value_ptr.record_names.put(
+                    bc.allocator,
+                    std.math.cast(u32, record.operands[0]) orelse return error.Overflow,
+                    name,
+                );
+            },
+            else => return error.UnsupportedBlockInfoRecord,
+        }
+    }
+}
+
+fn align32Bits(bc: *BitcodeReader) void {
+    bc.bit_offset = 0;
+}
+
+fn read32Bits(bc: *BitcodeReader) !u32 {
+    assert(bc.bit_offset == 0);
+    return bc.reader.readInt(u32, .little);
+}
+
+fn readBytes(bc: *BitcodeReader, bytes: []u8) !void {
+    assert(bc.bit_offset == 0);
+    try bc.reader.readNoEof(bytes);
+
+    const trailing_bytes = bytes.len % 4;
+    if (trailing_bytes > 0) {
+        var bit_buffer = [1]u8{0} ** 4;
+        try bc.reader.readNoEof(bit_buffer[trailing_bytes..]);
+        bc.bit_buffer = std.mem.readInt(u32, &bit_buffer, .little);
+        bc.bit_offset = @intCast(trailing_bytes * 8);
+    }
+}
+
+fn readFixed(bc: *BitcodeReader, comptime T: type, bits: u7) !T {
+    var result: T = 0;
+    var shift: std.math.Log2IntCeil(T) = 0;
+    var remaining = bits;
+    while (remaining > 0) {
+        if (bc.bit_offset == 0) bc.bit_buffer = try bc.read32Bits();
+        const chunk_len = @min(@as(u6, 32) - bc.bit_offset, remaining);
+        const chunk_mask = @as(u32, std.math.maxInt(u32)) >> @intCast(32 - chunk_len);
+        result |= @as(T, @intCast(bc.bit_buffer >> bc.bit_offset & chunk_mask)) << @intCast(shift);
+        shift += @intCast(chunk_len);
+        remaining -= chunk_len;
+        bc.bit_offset = @truncate(bc.bit_offset + chunk_len);
+    }
+    return result;
+}
+
+fn readVbr(bc: *BitcodeReader, comptime T: type, bits: u7) !T {
+    const chunk_bits: u6 = @intCast(bits - 1);
+    const chunk_msb = @as(u64, 1) << chunk_bits;
+
+    var result: u64 = 0;
+    var shift: u6 = 0;
+    while (true) {
+        var chunk = try bc.readFixed(u64, bits);
+        result |= (chunk & (chunk_msb - 1)) << shift;
+        if (chunk & chunk_msb == 0) break;
+        shift += chunk_bits;
+    }
+    return @intCast(result);
+}
+
+fn readChar6(bc: *BitcodeReader) !u8 {
+    return switch (try bc.readFixed(u6, 6)) {
+        0...25 => |c| @as(u8, c - 0) + 'a',
+        26...51 => |c| @as(u8, c - 26) + 'A',
+        52...61 => |c| @as(u8, c - 52) + '0',
+        62 => '.',
+        63 => '_',
+    };
+}
+
+const State = struct {
+    block_id: ?u32,
+    abbrev_id_width: u6,
+    abbrevs: Abbrev.Store,
+
+    fn deinit(state: *State, allocator: std.mem.Allocator) void {
+        state.abbrevs.deinit(allocator);
+        state.* = undefined;
+    }
+};
+
+const Abbrev = struct {
+    operands: []const Operand,
+
+    const Builtin = enum(u2) {
+        end_block,
+        enter_subblock,
+        define_abbrev,
+        unabbrev_record,
+
+        const first_record_id: u32 = std.math.maxInt(u32) - @typeInfo(Builtin).Enum.fields.len + 1;
+        fn toRecordId(builtin: Builtin) u32 {
+            return first_record_id + @intFromEnum(builtin);
+        }
+    };
+
+    const Operand = union(enum) {
+        literal: u64,
+        encoding: union(Encoding) {
+            fixed: u7,
+            vbr: u6,
+            array: u3,
+            char6,
+            blob: u3,
+        },
+        align_32_bits,
+        block_len,
+        abbrev_op,
+
+        const literal = std.math.maxInt(u64);
+        const Encoding = enum(u3) {
+            fixed = 1,
+            vbr = 2,
+            array = 3,
+            char6 = 4,
+            blob = 5,
+            _,
+        };
+    };
+
+    const Store = struct {
+        abbrevs: std.ArrayListUnmanaged(Abbrev),
+
+        fn deinit(store: *Store, allocator: std.mem.Allocator) void {
+            for (store.abbrevs.items) |abbrev| allocator.free(abbrev.operands);
+            store.abbrevs.deinit(allocator);
+            store.* = undefined;
+        }
+
+        fn addAbbrev(store: *Store, allocator: std.mem.Allocator, abbrev: Abbrev) !void {
+            try store.ensureUnusedCapacity(allocator, 1);
+            store.addAbbrevAssumeCapacity(abbrev);
+        }
+
+        fn addAbbrevAssumeCapacity(store: *Store, allocator: std.mem.Allocator, abbrev: Abbrev) !void {
+            store.abbrevs.appendAssumeCapacity(.{
+                .operands = try allocator.dupe(Abbrev.Operand, abbrev.operands),
+            });
+        }
+
+        fn addOwnedAbbrev(store: *Store, allocator: std.mem.Allocator, abbrev: Abbrev) !void {
+            try store.abbrevs.ensureUnusedCapacity(allocator, 1);
+            store.addOwnedAbbrevAssumeCapacity(abbrev);
+        }
+
+        fn addOwnedAbbrevAssumeCapacity(store: *Store, abbrev: Abbrev) void {
+            store.abbrevs.appendAssumeCapacity(abbrev);
+        }
+    };
+};
+
+const assert = std.debug.assert;
+const std = @import("std");
+
+const BitcodeReader = @This();
src/Compilation.zig
@@ -77,7 +77,7 @@ embed_file_work_queue: std.fifo.LinearFifo(*Module.EmbedFile, .Dynamic),
 
 /// The ErrorMsg memory is owned by the `CObject`, using Compilation's general purpose allocator.
 /// This data is accessed by multiple threads and is protected by `mutex`.
-failed_c_objects: std.AutoArrayHashMapUnmanaged(*CObject, *CObject.ErrorMsg) = .{},
+failed_c_objects: std.AutoArrayHashMapUnmanaged(*CObject, *CObject.Diag.Bundle) = .{},
 
 /// The ErrorBundle memory is owned by the `Win32Resource`, using Compilation's general purpose allocator.
 /// This data is accessed by multiple threads and is protected by `mutex`.
@@ -318,15 +318,286 @@ pub const CObject = struct {
         failure_retryable,
     },
 
-    pub const ErrorMsg = struct {
-        msg: []const u8,
-        line: u32,
-        column: u32,
+    pub const Diag = struct {
+        level: u32 = 0,
+        category: u32 = 0,
+        msg: []const u8 = &.{},
+        src_loc: SrcLoc = .{},
+        src_ranges: []const SrcRange = &.{},
+        sub_diags: []const Diag = &.{},
+
+        pub const SrcLoc = struct {
+            file: u32 = 0,
+            line: u32 = 0,
+            column: u32 = 0,
+            offset: u32 = 0,
+        };
+
+        pub const SrcRange = struct {
+            start: SrcLoc = .{},
+            end: SrcLoc = .{},
+        };
+
+        pub fn deinit(diag: *Diag, gpa: Allocator) void {
+            gpa.free(diag.msg);
+            gpa.free(diag.src_ranges);
+            for (diag.sub_diags) |sub_diag| {
+                var sub_diag_mut = sub_diag;
+                sub_diag_mut.deinit(gpa);
+            }
+            gpa.free(diag.sub_diags);
+            diag.* = undefined;
+        }
 
-        pub fn destroy(em: *ErrorMsg, gpa: Allocator) void {
-            gpa.free(em.msg);
-            gpa.destroy(em);
+        pub fn count(diag: Diag) u32 {
+            var total: u32 = 1;
+            for (diag.sub_diags) |sub_diag| total += sub_diag.count();
+            return total;
         }
+
+        pub fn addToErrorBundle(diag: Diag, eb: *ErrorBundle.Wip, bundle: Bundle, note: *u32) !void {
+            const err_msg = try eb.addErrorMessage(try diag.toErrorMessage(eb, bundle, 0));
+            eb.extra.items[note.*] = @intFromEnum(err_msg);
+            note.* += 1;
+            for (diag.sub_diags) |sub_diag| try sub_diag.addToErrorBundle(eb, bundle, note);
+        }
+
+        pub fn toErrorMessage(
+            diag: Diag,
+            eb: *ErrorBundle.Wip,
+            bundle: Bundle,
+            notes_len: u32,
+        ) !ErrorBundle.ErrorMessage {
+            var start = diag.src_loc.offset;
+            var end = diag.src_loc.offset;
+            for (diag.src_ranges) |src_range| {
+                if (src_range.start.file == diag.src_loc.file and
+                    src_range.start.line == diag.src_loc.line)
+                {
+                    start = @min(src_range.start.offset, start);
+                }
+                if (src_range.end.file == diag.src_loc.file and
+                    src_range.end.line == diag.src_loc.line)
+                {
+                    end = @max(src_range.end.offset, end);
+                }
+            }
+
+            const file_name = bundle.file_names.get(diag.src_loc.file) orelse "";
+            const source_line = source_line: {
+                if (diag.src_loc.offset == 0 or diag.src_loc.column == 0) break :source_line 0;
+
+                const file = std.fs.cwd().openFile(file_name, .{}) catch break :source_line 0;
+                defer file.close();
+                file.seekTo(diag.src_loc.offset + 1 - diag.src_loc.column) catch break :source_line 0;
+
+                var line = std.ArrayList(u8).init(eb.gpa);
+                defer line.deinit();
+                file.reader().readUntilDelimiterArrayList(&line, '\n', 1 << 10) catch break :source_line 0;
+
+                break :source_line try eb.addString(line.items);
+            };
+
+            return .{
+                .msg = try eb.addString(diag.msg),
+                .src_loc = try eb.addSourceLocation(.{
+                    .src_path = try eb.addString(file_name),
+                    .line = diag.src_loc.line -| 1,
+                    .column = diag.src_loc.column -| 1,
+                    .span_start = start,
+                    .span_main = diag.src_loc.offset,
+                    .span_end = end + 1,
+                    .source_line = source_line,
+                }),
+                .notes_len = notes_len,
+            };
+        }
+
+        pub const Bundle = struct {
+            file_names: std.AutoHashMapUnmanaged(u32, []const u8) = .{},
+            category_names: std.AutoHashMapUnmanaged(u32, []const u8) = .{},
+            diags: []Diag = &.{},
+
+            pub fn destroy(bundle: *Bundle, gpa: Allocator) void {
+                var file_name_it = bundle.file_names.valueIterator();
+                while (file_name_it.next()) |file_name| gpa.free(file_name.*);
+                bundle.file_names.deinit(gpa);
+
+                var category_name_it = bundle.category_names.valueIterator();
+                while (category_name_it.next()) |category_name| gpa.free(category_name.*);
+                bundle.category_names.deinit(gpa);
+
+                for (bundle.diags) |*diag| diag.deinit(gpa);
+                gpa.free(bundle.diags);
+
+                gpa.destroy(bundle);
+            }
+
+            pub fn parse(gpa: Allocator, path: []const u8) !*Bundle {
+                const BitcodeReader = @import("codegen/llvm/BitcodeReader.zig");
+                const BlockId = enum(u32) {
+                    Meta = 8,
+                    Diag,
+                    _,
+                };
+                const RecordId = enum(u32) {
+                    Version = 1,
+                    DiagInfo,
+                    SrcRange,
+                    DiagFlag,
+                    CatName,
+                    FileName,
+                    FixIt,
+                    _,
+                };
+                const WipDiag = struct {
+                    level: u32 = 0,
+                    category: u32 = 0,
+                    msg: []const u8 = &.{},
+                    src_loc: SrcLoc = .{},
+                    src_ranges: std.ArrayListUnmanaged(SrcRange) = .{},
+                    sub_diags: std.ArrayListUnmanaged(Diag) = .{},
+
+                    fn deinit(wip_diag: *@This(), allocator: Allocator) void {
+                        allocator.free(wip_diag.msg);
+                        wip_diag.src_ranges.deinit(allocator);
+                        for (wip_diag.sub_diags.items) |*sub_diag| sub_diag.deinit(allocator);
+                        wip_diag.sub_diags.deinit(allocator);
+                        wip_diag.* = undefined;
+                    }
+                };
+
+                const file = try std.fs.cwd().openFile(path, .{});
+                defer file.close();
+                var br = std.io.bufferedReader(file.reader());
+                const reader = br.reader();
+                var bc = BitcodeReader.init(gpa, .{ .reader = reader.any() });
+                defer bc.deinit();
+
+                var file_names: std.AutoHashMapUnmanaged(u32, []const u8) = .{};
+                errdefer {
+                    var file_name_it = file_names.valueIterator();
+                    while (file_name_it.next()) |file_name| gpa.free(file_name.*);
+                    file_names.deinit(gpa);
+                }
+
+                var category_names: std.AutoHashMapUnmanaged(u32, []const u8) = .{};
+                errdefer {
+                    var category_name_it = category_names.valueIterator();
+                    while (category_name_it.next()) |category_name| gpa.free(category_name.*);
+                    category_names.deinit(gpa);
+                }
+
+                var stack: std.ArrayListUnmanaged(WipDiag) = .{};
+                defer {
+                    for (stack.items) |*wip_diag| wip_diag.deinit(gpa);
+                    stack.deinit(gpa);
+                }
+                try stack.append(gpa, .{});
+
+                try bc.checkMagic("DIAG");
+                while (try bc.next()) |item| switch (item) {
+                    .start_block => |block| switch (@as(BlockId, @enumFromInt(block.id))) {
+                        .Meta => if (stack.items.len > 0) try bc.skipBlock(block),
+                        .Diag => try stack.append(gpa, .{}),
+                        _ => try bc.skipBlock(block),
+                    },
+                    .record => |record| switch (@as(RecordId, @enumFromInt(record.id))) {
+                        .Version => if (record.operands[0] != 2) return error.InvalidVersion,
+                        .DiagInfo => {
+                            const top = &stack.items[stack.items.len - 1];
+                            top.level = @intCast(record.operands[0]);
+                            top.src_loc = .{
+                                .file = @intCast(record.operands[1]),
+                                .line = @intCast(record.operands[2]),
+                                .column = @intCast(record.operands[3]),
+                                .offset = @intCast(record.operands[4]),
+                            };
+                            top.category = @intCast(record.operands[5]);
+                            top.msg = try gpa.dupe(u8, record.blob);
+                        },
+                        .SrcRange => try stack.items[stack.items.len - 1].src_ranges.append(gpa, .{
+                            .start = .{
+                                .file = @intCast(record.operands[0]),
+                                .line = @intCast(record.operands[1]),
+                                .column = @intCast(record.operands[2]),
+                                .offset = @intCast(record.operands[3]),
+                            },
+                            .end = .{
+                                .file = @intCast(record.operands[4]),
+                                .line = @intCast(record.operands[5]),
+                                .column = @intCast(record.operands[6]),
+                                .offset = @intCast(record.operands[7]),
+                            },
+                        }),
+                        .DiagFlag => {},
+                        .CatName => {
+                            try category_names.ensureUnusedCapacity(gpa, 1);
+                            category_names.putAssumeCapacity(
+                                @intCast(record.operands[0]),
+                                try gpa.dupe(u8, record.blob),
+                            );
+                        },
+                        .FileName => {
+                            try file_names.ensureUnusedCapacity(gpa, 1);
+                            file_names.putAssumeCapacity(
+                                @intCast(record.operands[0]),
+                                try gpa.dupe(u8, record.blob),
+                            );
+                        },
+                        .FixIt => {},
+                        _ => {},
+                    },
+                    .end_block => |block| switch (@as(BlockId, @enumFromInt(block.id))) {
+                        .Meta => {},
+                        .Diag => {
+                            var wip_diag = stack.pop();
+                            errdefer wip_diag.deinit(gpa);
+
+                            const src_ranges = try wip_diag.src_ranges.toOwnedSlice(gpa);
+                            errdefer gpa.free(src_ranges);
+
+                            const sub_diags = try wip_diag.sub_diags.toOwnedSlice(gpa);
+                            errdefer {
+                                for (sub_diags) |*sub_diag| sub_diag.deinit(gpa);
+                                gpa.free(sub_diags);
+                            }
+
+                            try stack.items[stack.items.len - 1].sub_diags.append(gpa, .{
+                                .level = wip_diag.level,
+                                .category = wip_diag.category,
+                                .msg = wip_diag.msg,
+                                .src_loc = wip_diag.src_loc,
+                                .src_ranges = src_ranges,
+                                .sub_diags = sub_diags,
+                            });
+                        },
+                        _ => {},
+                    },
+                };
+
+                const bundle = try gpa.create(Bundle);
+                assert(stack.items.len == 1);
+                bundle.* = .{
+                    .file_names = file_names,
+                    .category_names = category_names,
+                    .diags = try stack.items[0].sub_diags.toOwnedSlice(gpa),
+                };
+                return bundle;
+            }
+
+            pub fn addToErrorBundle(bundle: Bundle, eb: *ErrorBundle.Wip) !void {
+                for (bundle.diags) |diag| {
+                    const notes_len = diag.count() - 1;
+                    try eb.addRootErrorMessage(try diag.toErrorMessage(eb, bundle, notes_len));
+                    if (notes_len > 0) {
+                        var note = try eb.reserveNotes(notes_len);
+                        for (diag.sub_diags) |sub_diag|
+                            try sub_diag.addToErrorBundle(eb, bundle, &note);
+                    }
+                }
+            }
+        };
     };
 
     /// Returns if there was failure.
@@ -2826,11 +3097,16 @@ fn addBuf(bufs_list: []std.os.iovec_const, bufs_len: *usize, buf: []const u8) vo
 
 /// This function is temporally single-threaded.
 pub fn totalErrorCount(self: *Compilation) u32 {
-    var total: usize = self.failed_c_objects.count() +
+    var total: usize =
         self.misc_failures.count() +
         @intFromBool(self.alloc_failure_occurred) +
         self.lld_errors.items.len;
 
+    {
+        var it = self.failed_c_objects.iterator();
+        while (it.next()) |entry| total += entry.value_ptr.*.diags.len;
+    }
+
     if (!build_options.only_core_functionality) {
         for (self.failed_win32_resources.values()) |errs| {
             total += errs.errorMessageCount();
@@ -2911,24 +3187,7 @@ pub fn getAllErrorsAlloc(self: *Compilation) !ErrorBundle {
 
     {
         var it = self.failed_c_objects.iterator();
-        while (it.next()) |entry| {
-            const c_object = entry.key_ptr.*;
-            const err_msg = entry.value_ptr.*;
-            // TODO these fields will need to be adjusted when we have proper
-            // C error reporting bubbling up.
-            try bundle.addRootErrorMessage(.{
-                .msg = try bundle.printString("unable to build C object: {s}", .{err_msg.msg}),
-                .src_loc = try bundle.addSourceLocation(.{
-                    .src_path = try bundle.addString(c_object.src.src_path),
-                    .span_start = 0,
-                    .span_main = 0,
-                    .span_end = 1,
-                    .line = err_msg.line,
-                    .column = err_msg.column,
-                    .source_line = 0, // TODO
-                }),
-            });
-        }
+        while (it.next()) |entry| try entry.value_ptr.*.addToErrorBundle(&bundle);
     }
 
     if (!build_options.only_core_functionality) {
@@ -4209,19 +4468,9 @@ fn reportRetryableCObjectError(
 ) error{OutOfMemory}!void {
     c_object.status = .failure_retryable;
 
-    const c_obj_err_msg = try comp.gpa.create(CObject.ErrorMsg);
-    errdefer comp.gpa.destroy(c_obj_err_msg);
-    const msg = try std.fmt.allocPrint(comp.gpa, "{s}", .{@errorName(err)});
-    errdefer comp.gpa.free(msg);
-    c_obj_err_msg.* = .{
-        .msg = msg,
-        .line = 0,
-        .column = 0,
-    };
-    {
-        comp.mutex.lock();
-        defer comp.mutex.unlock();
-        try comp.failed_c_objects.putNoClobber(comp.gpa, c_object, c_obj_err_msg);
+    switch (comp.failCObj(c_object, "{s}", .{@errorName(err)})) {
+        error.AnalysisFail => return,
+        else => |e| return e,
     }
 }
 
@@ -4457,6 +4706,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: *std.P
         // We can't know the digest until we do the C compiler invocation,
         // so we need a temporary filename.
         const out_obj_path = try comp.tmpFilePath(arena, o_basename);
+        const out_diag_path = try std.fmt.allocPrint(arena, "{s}.diag", .{out_obj_path});
         var zig_cache_tmp_dir = try comp.local_cache_directory.handle.makeOpenPath("tmp", .{});
         defer zig_cache_tmp_dir.close();
 
@@ -4470,18 +4720,20 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: *std.P
 
         try argv.ensureUnusedCapacity(5);
         switch (comp.clang_preprocessor_mode) {
-            .no => argv.appendSliceAssumeCapacity(&[_][]const u8{ "-c", "-o", out_obj_path }),
-            .yes => argv.appendSliceAssumeCapacity(&[_][]const u8{ "-E", "-o", out_obj_path }),
+            .no => argv.appendSliceAssumeCapacity(&.{ "-c", "-o", out_obj_path }),
+            .yes => argv.appendSliceAssumeCapacity(&.{ "-E", "-o", out_obj_path }),
             .stdout => argv.appendAssumeCapacity("-E"),
         }
         if (comp.clang_passthrough_mode) {
             if (comp.emit_asm != null) {
                 argv.appendAssumeCapacity("-S");
             } else if (comp.emit_llvm_ir != null) {
-                argv.appendSliceAssumeCapacity(&[_][]const u8{ "-emit-llvm", "-S" });
+                argv.appendSliceAssumeCapacity(&.{ "-emit-llvm", "-S" });
             } else if (comp.emit_llvm_bc != null) {
                 argv.appendAssumeCapacity("-emit-llvm");
             }
+        } else {
+            argv.appendSliceAssumeCapacity(&.{ "--serialize-diagnostics", out_diag_path });
         }
 
         if (comp.verbose_cc) {
@@ -4524,10 +4776,11 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: *std.P
                 switch (term) {
                     .Exited => |code| {
                         if (code != 0) {
-                            // TODO parse clang stderr and turn it into an error message
-                            // and then call failCObjWithOwnedErrorMsg
-                            log.err("clang failed with stderr: {s}", .{stderr});
-                            return comp.failCObj(c_object, "clang exited with code {d}", .{code});
+                            const bundle = CObject.Diag.Bundle.parse(comp.gpa, out_diag_path) catch |err| {
+                                log.err("{}: failed to parse clang diagnostics: {s}", .{ err, stderr });
+                                return comp.failCObj(c_object, "clang exited with code {d}", .{code});
+                            };
+                            return comp.failCObjWithOwnedDiagBundle(c_object, bundle);
                         }
                     },
                     else => {
@@ -5413,37 +5666,45 @@ pub fn addCCArgs(
     try argv.appendSlice(comp.clang_argv);
 }
 
-fn failCObj(comp: *Compilation, c_object: *CObject, comptime format: []const u8, args: anytype) SemaError {
+fn failCObj(
+    comp: *Compilation,
+    c_object: *CObject,
+    comptime format: []const u8,
+    args: anytype,
+) SemaError {
     @setCold(true);
-    const err_msg = blk: {
-        const msg = try std.fmt.allocPrint(comp.gpa, format, args);
-        errdefer comp.gpa.free(msg);
-        const err_msg = try comp.gpa.create(CObject.ErrorMsg);
-        errdefer comp.gpa.destroy(err_msg);
-        err_msg.* = .{
-            .msg = msg,
-            .line = 0,
-            .column = 0,
-        };
-        break :blk err_msg;
+    const diag_bundle = blk: {
+        const diag_bundle = try comp.gpa.create(CObject.Diag.Bundle);
+        diag_bundle.* = .{};
+        errdefer diag_bundle.destroy(comp.gpa);
+
+        try diag_bundle.file_names.ensureTotalCapacity(comp.gpa, 1);
+        diag_bundle.file_names.putAssumeCapacity(1, try comp.gpa.dupe(u8, c_object.src.src_path));
+
+        diag_bundle.diags = try comp.gpa.alloc(CObject.Diag, 1);
+        diag_bundle.diags[0] = .{};
+        diag_bundle.diags[0].level = 3;
+        diag_bundle.diags[0].msg = try std.fmt.allocPrint(comp.gpa, format, args);
+        diag_bundle.diags[0].src_loc.file = 1;
+        break :blk diag_bundle;
     };
-    return comp.failCObjWithOwnedErrorMsg(c_object, err_msg);
+    return comp.failCObjWithOwnedDiagBundle(c_object, diag_bundle);
 }
 
-fn failCObjWithOwnedErrorMsg(
+fn failCObjWithOwnedDiagBundle(
     comp: *Compilation,
     c_object: *CObject,
-    err_msg: *CObject.ErrorMsg,
+    diag_bundle: *CObject.Diag.Bundle,
 ) SemaError {
     @setCold(true);
     {
         comp.mutex.lock();
         defer comp.mutex.unlock();
         {
-            errdefer err_msg.destroy(comp.gpa);
+            errdefer diag_bundle.destroy(comp.gpa);
             try comp.failed_c_objects.ensureUnusedCapacity(comp.gpa, 1);
         }
-        comp.failed_c_objects.putAssumeCapacityNoClobber(c_object, err_msg);
+        comp.failed_c_objects.putAssumeCapacityNoClobber(c_object, diag_bundle);
     }
     c_object.status = .failure;
     return error.AnalysisFail;