|
//! 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 |
string_bytes: []const u8, /// The first thing in this array is an `ErrorMessageList`. extra: []const u32, |
MessageIndexSpecial encoding when there are no errors. |
/// Special encoding when there are no errors. pub const empty: ErrorBundle = .{ .string_bytes = &.{}, .extra = &.{}, }; |
SourceLocationIndexThere will be a MessageIndex for each len at start. |
// An index into `extra` pointing at an `ErrorMessage`. pub const MessageIndex = enum(u32) { _, }; |
ErrorMessageListnull-terminated string index. 0 means no compile log text. |
// An index into `extra` pointing at an `SourceLocation`. pub const SourceLocationIndex = enum(u32) { none = 0, _, }; |
SourceLocationTrailing: * 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, }; |
ErrorMessagenull 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, }; |
ReferenceTracebyte 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); } |
RenderOptionsGiven an index into |
/// 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 |
/// 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; } } }; |
Generated by zstd-live on 2025-08-13 02:35:14 UTC. |