One of the most commonly cited weaknesses of Zig is that error unions don't have an error payload, unlike Rust's Result
. So, human-readable error information is usually either passed around with a Diagnostics
struct, shat to stdout, or not there at all.
Here's an example of a Diagnostics
from std.json:
/// To enable diagnostics, declare `var diagnostics = Diagnostics{};` then call `source.enableDiagnostics(&diagnostics);`
/// where `source` is either a `std.json.Reader` or a `std.json.Scanner` that has just been initialized.
/// At any time, notably just after an error, call `getLine()`, `getColumn()`, and/or `getByteOffset()`
/// to get meaningful information from this.
pub const Diagnostics = struct {
line_number: u64 = 1,
line_start_cursor: usize = @as(usize, @bitCast(@as(isize, -1))), // Start just "before" the input buffer to get a 1-based column for line 1.
total_bytes_before_current_input: u64 = 0,
cursor_pointer: *const usize = undefined,
/// Starts at 1.
pub fn getLine(self: *const @This()) u64 {
return self.line_number;
}
/// Starts at 1.
pub fn getColumn(self: *const @This()) u64 {
return self.cursor_pointer.* -% self.line_start_cursor;
}
/// Starts at 0. Measures the byte offset since the start of the input.
pub fn getByteOffset(self: *const @This()) u64 {
return self.total_bytes_before_current_input + self.cursor_pointer.*;
}
};
I want to share a more general and convenient implementation of this pattern I am writing for a work-in-progress multimedia library:
/// A single diagnostic message.
pub const Diagnostic = struct {
pub const Component = enum {
component_a,
component_b,
// ....
};
/// The severity of the failure.
level: std.log.Level,
/// The component this failure occurred in.
component: Component,
/// A human-readable description of the failure.
message: std.BoundedArray(u8, 512),
/// The machine-readable Zig error for this failure.
err: anyerror,
pub inline fn diagFmt(d: *Diagnostic, level: std.log.Level, component: Component, err: anyerror, comptime fmt: []const u8, args: anytype) void {
d.level = level;
d.component = component;
d.err = err;
d.message.len = @intCast((std.fmt.bufPrint(&d.message.buffer, fmt, args) catch "").len);
}
pub fn format(d: Diagnostic, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
return writer.print("[{s}] {s}: {s} ({})", .{ @tagName(d.level), @tagName(d.component), d.message.constSlice(), d.err });
}
};
pub const Diagnostics = struct {
list: std.ArrayList(Diagnostic),
pub fn init(ally: std.mem.Allocator) Diagnostics {
return .{ .list = std.ArrayList(Diagnostic).init(ally) };
}
pub fn deinit(d: *Diagnostics) void {
d.list.deinit();
}
fn DiagRet(comptime Err: type) type {
return if (@typeInfo(Err) == .ErrorSet) Err!noreturn else void;
}
// This is a piece of hackery that allows for fatal and non-fatal errors to be logged easily. `err` can be either {} or an error.
pub inline fn diag(
d: *Diagnostics,
level: std.log.Level,
component: Diagnostic.Component,
err: anytype,
comptime fmt: []const u8,
args: anytype,
) DiagRet(@TypeOf(err)) {
const p = d.list.addOne() catch return err;
p.diagFmt(level, component, if (@typeInfo(@TypeOf(err)) == .ErrorSet) err else error.Unexpected, fmt, args);
return err;
}
};
And here it is in use for errors and warnings:
try z.diag(
.err,
.decoder,
e,
"Ran out of memory when allocating a frame of size {d}x{d} (sample format {}).",
.{ width.?, height.?, sample_fmt },
);
z.diag(.warn, .decoder, {}, "The {s} field was duplicated.", .{"width"});
Caveats:
- The size of the message is limited.
- This does not include a stack trace. You can always add one, though.
- The "error" is always there, even when no error has been specified.
Top comments (0)