Zig NEWS

cancername
cancername

Posted on • Originally published at cancername.neocities.org

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.*;
    }
};
Enter fullscreen mode Exit fullscreen mode

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;
    }
};
Enter fullscreen mode Exit fullscreen mode

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 },
);
Enter fullscreen mode Exit fullscreen mode
z.diag(.warn, .decoder, {}, "The {s} field was duplicated.", .{"width"});
Enter fullscreen mode Exit fullscreen mode

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)