zig/lib/std / zig/ErrorBundle.zig

To support incremental compilation, errors are stored in various places so that they can be created and destroyed appropriately. This structure is used to collect all the errors from the various places into one convenient place for API users to consume. There is one special encoding for this data structure. If both arrays are empty, it means there are no errors. This special encoding exists so that heap allocation is not needed in the common case of no errors.

//! To support incremental compilation, errors are stored in various places
//! so that they can be created and destroyed appropriately. This structure
//! is used to collect all the errors from the various places into one
//! convenient place for API users to consume.
//!
//! There is one special encoding for this data structure. If both arrays are
//! empty, it means there are no errors. This special encoding exists so that
//! heap allocation is not needed in the common case of no errors.

empty:

The first thing in this array is an ErrorMessageList.


string_bytes: []const u8,
/// The first thing in this array is an `ErrorMessageList`.
extra: []const u32,

MessageIndex

Special encoding when there are no errors.


/// Special encoding when there are no errors.
pub const empty: ErrorBundle = .{
    .string_bytes = &.{},
    .extra = &.{},
};

SourceLocationIndex

There will be a MessageIndex for each len at start.


// An index into `extra` pointing at an `ErrorMessage`.
pub const MessageIndex = enum(u32) {
    _,
};

ErrorMessageList

null-terminated string index. 0 means no compile log text.


// An index into `extra` pointing at an `SourceLocation`.
pub const SourceLocationIndex = enum(u32) {
    none = 0,
    _,
};

SourceLocation

Trailing: * ReferenceTrace for each reference_trace_len


/// There will be a MessageIndex for each len at start.
pub const ErrorMessageList = struct {
    len: u32,
    start: u32,
    /// null-terminated string index. 0 means no compile log text.
    compile_log_text: u32,
};

ErrorMessage

null terminated string index


/// Trailing:
/// * ReferenceTrace for each reference_trace_len
pub const SourceLocation = struct {
    /// null terminated string index
    src_path: u32,
    line: u32,
    column: u32,
    /// byte offset of starting token
    span_start: u32,
    /// byte offset of main error location
    span_main: u32,
    /// byte offset of end of last token
    span_end: u32,
    /// null terminated string index, possibly null.
    /// Does not include the trailing newline.
    source_line: u32 = 0,
    reference_trace_len: u32 = 0,
};

ReferenceTrace

byte offset of starting token


/// Trailing:
/// * MessageIndex for each notes_len.
pub const ErrorMessage = struct {
    /// null terminated string index
    msg: u32,
    /// Usually one, but incremented for redundant messages.
    count: u32 = 1,
    src_loc: SourceLocationIndex = .none,
    notes_len: u32 = 0,
};

deinit()

byte offset of main error location


pub const ReferenceTrace = struct {
    /// null terminated string index
    /// Except for the sentinel ReferenceTrace element, in which case:
    /// * 0 means remaining references hidden
    /// * >0 means N references hidden
    decl_name: u32,
    /// Index into extra of a SourceLocation
    /// If this is 0, this is the sentinel ReferenceTrace element.
    src_loc: SourceLocationIndex,
};

errorMessageCount()

byte offset of end of last token


pub fn deinit(eb: *ErrorBundle, gpa: Allocator) void {
    gpa.free(eb.string_bytes);
    gpa.free(eb.extra);
    eb.* = undefined;
}

getErrorMessageList()

null terminated string index, possibly null. Does not include the trailing newline.


pub fn errorMessageCount(eb: ErrorBundle) u32 {
    if (eb.extra.len == 0) return 0;
    return eb.getErrorMessageList().len;
}

getMessages()

Trailing: * MessageIndex for each notes_len.


pub fn getErrorMessageList(eb: ErrorBundle) ErrorMessageList {
    return eb.extraData(ErrorMessageList, 0).data;
}

getErrorMessage()

null terminated string index


pub fn getMessages(eb: ErrorBundle) []const MessageIndex {
    const list = eb.getErrorMessageList();
    return @as([]const MessageIndex, @ptrCast(eb.extra[list.start..][0..list.len]));
}

getSourceLocation()

Usually one, but incremented for redundant messages.


pub fn getErrorMessage(eb: ErrorBundle, index: MessageIndex) ErrorMessage {
    return eb.extraData(ErrorMessage, @intFromEnum(index)).data;
}

getNotes()

null terminated string index Except for the sentinel ReferenceTrace element, in which case: * 0 means remaining references hidden * >0 means N references hidden


pub fn getSourceLocation(eb: ErrorBundle, index: SourceLocationIndex) SourceLocation {
    assert(index != .none);
    return eb.extraData(SourceLocation, @intFromEnum(index)).data;
}

getCompileLogOutput()

Index into extra of a SourceLocation If this is 0, this is the sentinel ReferenceTrace element.


pub fn getNotes(eb: ErrorBundle, index: MessageIndex) []const MessageIndex {
    const notes_len = eb.getErrorMessage(index).notes_len;
    const start = @intFromEnum(index) + @typeInfo(ErrorMessage).Struct.fields.len;
    return @as([]const MessageIndex, @ptrCast(eb.extra[start..][0..notes_len]));
}

nullTerminatedString()

Returns the requested data, as well as the new index which is at the start of the trailers for the object.


pub fn getCompileLogOutput(eb: ErrorBundle) [:0]const u8 {
    return nullTerminatedString(eb, getErrorMessageList(eb).compile_log_text);
}

RenderOptions

Given an index into string_bytes returns the null-terminated string found there.


/// Returns the requested data, as well as the new index which is at the start of the
/// trailers for the object.
fn extraData(eb: ErrorBundle, comptime T: type, index: usize) struct { data: T, end: usize } {
    const fields = @typeInfo(T).Struct.fields;
    var i: usize = index;
    var result: T = undefined;
    inline for (fields) |field| {
        @field(result, field.name) = switch (field.type) {
            u32 => eb.extra[i],
            MessageIndex => @as(MessageIndex, @enumFromInt(eb.extra[i])),
            SourceLocationIndex => @as(SourceLocationIndex, @enumFromInt(eb.extra[i])),
            else => @compileError("bad field type"),
        };
        i += 1;
    }
    return .{
        .data = result,
        .end = i,
    };
}

renderToStdErr()

Splits the error message up into lines to properly indent them to allow for long, good-looking error messages. This is used to split the message in @compileError("hello\nworld") for example.


/// Given an index into `string_bytes` returns the null-terminated string found there.
pub fn nullTerminatedString(eb: ErrorBundle, index: usize) [:0]const u8 {
    const string_bytes = eb.string_bytes;
    var end: usize = index;
    while (string_bytes[end] != 0) {
        end += 1;
    }
    return string_bytes[index..end :0];
}

renderToWriter()

The first thing in this array is a ErrorMessageList.


pub const RenderOptions = struct {
    ttyconf: std.io.tty.Config,
    include_reference_trace: bool = true,
    include_source_line: bool = true,
    include_log_text: bool = true,
};

Wip


pub fn renderToStdErr(eb: ErrorBundle, options: RenderOptions) void {
    std.debug.getStderrMutex().lock();
    defer std.debug.getStderrMutex().unlock();
    const stderr = std.io.getStdErr();
    return renderToWriter(eb, options, stderr.writer()) catch return;
}

init()


pub fn renderToWriter(eb: ErrorBundle, options: RenderOptions, writer: anytype) anyerror!void {
    for (eb.getMessages()) |err_msg| {
        try renderErrorMessageToWriter(eb, options, err_msg, writer, "error", .red, 0);
    }

deinit()


    if (options.include_log_text) {
        const log_text = eb.getCompileLogOutput();
        if (log_text.len != 0) {
            try writer.writeAll("\nCompile Log Output:\n");
            try writer.writeAll(log_text);
        }
    }
}

toOwnedBundle()


fn renderErrorMessageToWriter(
    eb: ErrorBundle,
    options: RenderOptions,
    err_msg_index: MessageIndex,
    stderr: anytype,
    kind: []const u8,
    color: std.io.tty.Color,
    indent: usize,
) anyerror!void {
    const ttyconf = options.ttyconf;
    var counting_writer = std.io.countingWriter(stderr);
    const counting_stderr = counting_writer.writer();
    const err_msg = eb.getErrorMessage(err_msg_index);
    if (err_msg.src_loc != .none) {
        const src = eb.extraData(SourceLocation, @intFromEnum(err_msg.src_loc));
        try counting_stderr.writeByteNTimes(' ', indent);
        try ttyconf.setColor(stderr, .bold);
        try counting_stderr.print("{s}:{d}:{d}: ", .{
            eb.nullTerminatedString(src.data.src_path),
            src.data.line + 1,
            src.data.column + 1,
        });
        try ttyconf.setColor(stderr, color);
        try counting_stderr.writeAll(kind);
        try counting_stderr.writeAll(": ");
        // This is the length of the part before the error message:
        // e.g. "file.zig:4:5: error: "
        const prefix_len = @as(usize, @intCast(counting_stderr.context.bytes_written));
        try ttyconf.setColor(stderr, .reset);
        try ttyconf.setColor(stderr, .bold);
        if (err_msg.count == 1) {
            try writeMsg(eb, err_msg, stderr, prefix_len);
            try stderr.writeByte('\n');
        } else {
            try writeMsg(eb, err_msg, stderr, prefix_len);
            try ttyconf.setColor(stderr, .dim);
            try stderr.print(" ({d} times)\n", .{err_msg.count});
        }
        try ttyconf.setColor(stderr, .reset);
        if (src.data.source_line != 0 and options.include_source_line) {
            const line = eb.nullTerminatedString(src.data.source_line);
            for (line) |b| switch (b) {
                '\t' => try stderr.writeByte(' '),
                else => try stderr.writeByte(b),
            };
            try stderr.writeByte('\n');
            // TODO basic unicode code point monospace width
            const before_caret = src.data.span_main - src.data.span_start;
            // -1 since span.main includes the caret
            const after_caret = src.data.span_end -| src.data.span_main -| 1;
            try stderr.writeByteNTimes(' ', src.data.column - before_caret);
            try ttyconf.setColor(stderr, .green);
            try stderr.writeByteNTimes('~', before_caret);
            try stderr.writeByte('^');
            try stderr.writeByteNTimes('~', after_caret);
            try stderr.writeByte('\n');
            try ttyconf.setColor(stderr, .reset);
        }
        for (eb.getNotes(err_msg_index)) |note| {
            try renderErrorMessageToWriter(eb, options, note, stderr, "note", .cyan, indent);
        }
        if (src.data.reference_trace_len > 0 and options.include_reference_trace) {
            try ttyconf.setColor(stderr, .reset);
            try ttyconf.setColor(stderr, .dim);
            try stderr.print("referenced by:\n", .{});
            var ref_index = src.end;
            for (0..src.data.reference_trace_len) |_| {
                const ref_trace = eb.extraData(ReferenceTrace, ref_index);
                ref_index = ref_trace.end;
                if (ref_trace.data.src_loc != .none) {
                    const ref_src = eb.getSourceLocation(ref_trace.data.src_loc);
                    try stderr.print("    {s}: {s}:{d}:{d}\n", .{
                        eb.nullTerminatedString(ref_trace.data.decl_name),
                        eb.nullTerminatedString(ref_src.src_path),
                        ref_src.line + 1,
                        ref_src.column + 1,
                    });
                } else if (ref_trace.data.decl_name != 0) {
                    const count = ref_trace.data.decl_name;
                    try stderr.print(
                        "    {d} reference(s) hidden; use '-freference-trace={d}' to see all references\n",
                        .{ count, count + src.data.reference_trace_len - 1 },
                    );
                } else {
                    try stderr.print(
                        "    remaining reference traces hidden; use '-freference-trace' to see all reference traces\n",
                        .{},
                    );
                }
            }
            try ttyconf.setColor(stderr, .reset);
        }
    } else {
        try ttyconf.setColor(stderr, color);
        try stderr.writeByteNTimes(' ', indent);
        try stderr.writeAll(kind);
        try stderr.writeAll(": ");
        try ttyconf.setColor(stderr, .reset);
        const msg = eb.nullTerminatedString(err_msg.msg);
        if (err_msg.count == 1) {
            try stderr.print("{s}\n", .{msg});
        } else {
            try stderr.print("{s}", .{msg});
            try ttyconf.setColor(stderr, .dim);
            try stderr.print(" ({d} times)\n", .{err_msg.count});
        }
        try ttyconf.setColor(stderr, .reset);
        for (eb.getNotes(err_msg_index)) |note| {
            try renderErrorMessageToWriter(eb, options, note, stderr, "note", .cyan, indent + 4);
        }
    }
}

tmpBundle()


/// Splits the error message up into lines to properly indent them
/// to allow for long, good-looking error messages.
///
/// This is used to split the message in `@compileError("hello\nworld")` for example.
fn writeMsg(eb: ErrorBundle, err_msg: ErrorMessage, stderr: anytype, indent: usize) !void {
    var lines = std.mem.splitScalar(u8, eb.nullTerminatedString(err_msg.msg), '\n');
    while (lines.next()) |line| {
        try stderr.writeAll(line);
        if (lines.index == null) break;
        try stderr.writeByte('\n');
        try stderr.writeByteNTimes(' ', indent);
    }
}

addString()


const std = @import("std");
const ErrorBundle = @This();
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;

printString()


pub const Wip = struct {
    gpa: Allocator,
    string_bytes: std.ArrayListUnmanaged(u8),
    /// The first thing in this array is a ErrorMessageList.
    extra: std.ArrayListUnmanaged(u32),
    root_list: std.ArrayListUnmanaged(MessageIndex),

addRootErrorMessage()


    pub fn init(wip: *Wip, gpa: Allocator) !void {
        wip.* = .{
            .gpa = gpa,
            .string_bytes = .{},
            .extra = .{},
            .root_list = .{},
        };

addErrorMessage()


        // So that 0 can be used to indicate a null string.
        try wip.string_bytes.append(gpa, 0);

addErrorMessageAssumeCapacity()


        assert(0 == try addExtra(wip, ErrorMessageList{
            .len = 0,
            .start = 0,
            .compile_log_text = 0,
        }));
    }

addSourceLocation()


    pub fn deinit(wip: *Wip) void {
        const gpa = wip.gpa;
        wip.root_list.deinit(gpa);
        wip.string_bytes.deinit(gpa);
        wip.extra.deinit(gpa);
        wip.* = undefined;
    }

addReferenceTrace()


    pub fn toOwnedBundle(wip: *Wip, compile_log_text: []const u8) !ErrorBundle {
        const gpa = wip.gpa;
        if (wip.root_list.items.len == 0) {
            assert(compile_log_text.len == 0);
            // Special encoding when there are no errors.
            wip.deinit();
            wip.* = .{
                .gpa = gpa,
                .string_bytes = .{},
                .extra = .{},
                .root_list = .{},
            };
            return empty;
        }

addBundle()


        const compile_log_str_index = if (compile_log_text.len == 0) 0 else str: {
            const str = @as(u32, @intCast(wip.string_bytes.items.len));
            try wip.string_bytes.ensureUnusedCapacity(gpa, compile_log_text.len + 1);
            wip.string_bytes.appendSliceAssumeCapacity(compile_log_text);
            wip.string_bytes.appendAssumeCapacity(0);
            break :str str;
        };

reserveNotes()


        wip.setExtra(0, ErrorMessageList{
            .len = @as(u32, @intCast(wip.root_list.items.len)),
            .start = @as(u32, @intCast(wip.extra.items.len)),
            .compile_log_text = compile_log_str_index,
        });
        try wip.extra.appendSlice(gpa, @as([]const u32, @ptrCast(wip.root_list.items)));
        wip.root_list.clearAndFree(gpa);
        return .{
            .string_bytes = try wip.string_bytes.toOwnedSlice(gpa),
            .extra = try wip.extra.toOwnedSlice(gpa),
        };
    }

    pub fn tmpBundle(wip: Wip) ErrorBundle {
        return .{
            .string_bytes = wip.string_bytes.items,
            .extra = wip.extra.items,
        };
    }

    pub fn addString(wip: *Wip, s: []const u8) !u32 {
        const gpa = wip.gpa;
        const index = @as(u32, @intCast(wip.string_bytes.items.len));
        try wip.string_bytes.ensureUnusedCapacity(gpa, s.len + 1);
        wip.string_bytes.appendSliceAssumeCapacity(s);
        wip.string_bytes.appendAssumeCapacity(0);
        return index;
    }

    pub fn printString(wip: *Wip, comptime fmt: []const u8, args: anytype) !u32 {
        const gpa = wip.gpa;
        const index = @as(u32, @intCast(wip.string_bytes.items.len));
        try wip.string_bytes.writer(gpa).print(fmt, args);
        try wip.string_bytes.append(gpa, 0);
        return index;
    }

    pub fn addRootErrorMessage(wip: *Wip, em: ErrorMessage) !void {
        try wip.root_list.ensureUnusedCapacity(wip.gpa, 1);
        wip.root_list.appendAssumeCapacity(try addErrorMessage(wip, em));
    }

    pub fn addErrorMessage(wip: *Wip, em: ErrorMessage) !MessageIndex {
        return @as(MessageIndex, @enumFromInt(try addExtra(wip, em)));
    }

    pub fn addErrorMessageAssumeCapacity(wip: *Wip, em: ErrorMessage) MessageIndex {
        return @as(MessageIndex, @enumFromInt(addExtraAssumeCapacity(wip, em)));
    }

    pub fn addSourceLocation(wip: *Wip, sl: SourceLocation) !SourceLocationIndex {
        return @as(SourceLocationIndex, @enumFromInt(try addExtra(wip, sl)));
    }

    pub fn addReferenceTrace(wip: *Wip, rt: ReferenceTrace) !void {
        _ = try addExtra(wip, rt);
    }

    pub fn addBundle(wip: *Wip, other: ErrorBundle) !void {
        const gpa = wip.gpa;

        try wip.string_bytes.ensureUnusedCapacity(gpa, other.string_bytes.len);
        try wip.extra.ensureUnusedCapacity(gpa, other.extra.len);

        const other_list = other.getMessages();

        // The ensureUnusedCapacity call above guarantees this.
        const notes_start = wip.reserveNotes(@as(u32, @intCast(other_list.len))) catch unreachable;
        for (notes_start.., other_list) |note, message| {
            wip.extra.items[note] = @intFromEnum(wip.addOtherMessage(other, message) catch unreachable);
        }
    }

    pub fn reserveNotes(wip: *Wip, notes_len: u32) !u32 {
        try wip.extra.ensureUnusedCapacity(wip.gpa, notes_len +
            notes_len * @typeInfo(ErrorBundle.ErrorMessage).Struct.fields.len);
        wip.extra.items.len += notes_len;
        return @as(u32, @intCast(wip.extra.items.len - notes_len));
    }

    fn addOtherMessage(wip: *Wip, other: ErrorBundle, msg_index: MessageIndex) !MessageIndex {
        const other_msg = other.getErrorMessage(msg_index);
        const src_loc = try wip.addOtherSourceLocation(other, other_msg.src_loc);
        const msg = try wip.addErrorMessage(.{
            .msg = try wip.addString(other.nullTerminatedString(other_msg.msg)),
            .count = other_msg.count,
            .src_loc = src_loc,
            .notes_len = other_msg.notes_len,
        });
        const notes_start = try wip.reserveNotes(other_msg.notes_len);
        for (notes_start.., other.getNotes(msg_index)) |note, other_note| {
            wip.extra.items[note] = @intFromEnum(try wip.addOtherMessage(other, other_note));
        }
        return msg;
    }

    fn addOtherSourceLocation(
        wip: *Wip,
        other: ErrorBundle,
        index: SourceLocationIndex,
    ) !SourceLocationIndex {
        if (index == .none) return .none;
        const other_sl = other.getSourceLocation(index);

        const src_loc = try wip.addSourceLocation(.{
            .src_path = try wip.addString(other.nullTerminatedString(other_sl.src_path)),
            .line = other_sl.line,
            .column = other_sl.column,
            .span_start = other_sl.span_start,
            .span_main = other_sl.span_main,
            .span_end = other_sl.span_end,
            .source_line = try wip.addString(other.nullTerminatedString(other_sl.source_line)),
            .reference_trace_len = other_sl.reference_trace_len,
        });

        // TODO: also add the reference trace

        return src_loc;
    }

    fn addExtra(wip: *Wip, extra: anytype) Allocator.Error!u32 {
        const gpa = wip.gpa;
        const fields = @typeInfo(@TypeOf(extra)).Struct.fields;
        try wip.extra.ensureUnusedCapacity(gpa, fields.len);
        return addExtraAssumeCapacity(wip, extra);
    }

    fn addExtraAssumeCapacity(wip: *Wip, extra: anytype) u32 {
        const fields = @typeInfo(@TypeOf(extra)).Struct.fields;
        const result = @as(u32, @intCast(wip.extra.items.len));
        wip.extra.items.len += fields.len;
        setExtra(wip, result, extra);
        return result;
    }

    fn setExtra(wip: *Wip, index: usize, extra: anytype) void {
        const fields = @typeInfo(@TypeOf(extra)).Struct.fields;
        var i = index;
        inline for (fields) |field| {
            wip.extra.items[i] = switch (field.type) {
                u32 => @field(extra, field.name),
                MessageIndex => @intFromEnum(@field(extra, field.name)),
                SourceLocationIndex => @intFromEnum(@field(extra, field.name)),
                else => @compileError("bad field type"),
            };
            i += 1;
        }
    }
};