Applying the Diagnostics pattern in practice
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.