Printing nested types with custom formatting

I can use f to print a type with a custom formatting function. Is there a format specifier that works like f, but recursively? Print with format if it exists, otherwise fall back to any. Basically, how it used to work in 0.14?

const std = @import("std");

const Duration = std.Io.Duration;

const DurationInterval = struct {
    min: Duration,
    max: Duration,
};

pub fn main() void {
    const d: Duration = .fromSeconds(92);

    const di: DurationInterval = .{
        .min = .fromSeconds(92),
        .max = .fromSeconds(100),
    };

    std.debug.print("{f}\n", .{d}); // Prints 1m32s, nice!
    std.debug.print("{}\n", .{di}); // Prints `.{ .min = .{ .nanoseconds =`, urgh.
    std.debug.print("{f}\n", .{di}); // Doesn't compile.
}

The use-case here is printf-debugging: I want to be able to dump human-readable representation of anything to stderr, without having to explicitly define the said representation for all intermediary data structures.

6 Likes

I’ve written this one before in 0.15.2. It still works with 0.16, but not on master:

const std = @import("std");

const Duration = std.Io.Duration;

const DurationInterval = struct {
    min: Duration,
    max: Duration,
};

pub fn main() void {
    const d: Duration = .fromSeconds(92);

    const di: DurationInterval = .{
        .min = .fromSeconds(92),
        .max = .fromSeconds(100),
    };

    std.debug.print("{f}\n", .{d}); // Prints 1m32s, nice!
    std.debug.print("{}\n", .{di}); // Prints `.{ .min = .{ .nanoseconds =`, urgh.
    std.debug.print("{f}\n", .{dump(di)}); // Prints `.{ .min = 1m32s, .max = 1m40s }`
}

/// Wraps any value in an anonymous formatter that recursively dispatches to `{f}` when a type
/// declares a custom `format` method, or falls back to reflection.
pub fn dump(val: anytype) struct {
    data: @TypeOf(val),

    fn formatDump(value: anytype, writer: *std.Io.Writer) std.Io.Writer.Error!void {
        const ValT = @TypeOf(value);
        switch (@typeInfo(ValT)) {
            .@"struct", .@"union", .@"enum" => {
                if (@hasDecl(ValT, "format")) {
                    return writer.print("{f}", .{value});
                }
            },
            else => {},
        }

        switch (@typeInfo(ValT)) {
            .@"struct" => |info| {
                try writer.writeAll(".{ ");
                inline for (info.fields, 0..) |field, i| {
                    if (i > 0) try writer.writeAll(", ");
                    if (!info.is_tuple) {
                        try writer.print(".{s} = ", .{field.name});
                    }
                    try formatDump(@field(value, field.name), writer);
                }
                try writer.writeAll(" }");
            },
            .optional => {
                if (value) |payload| {
                    try formatDump(payload, writer);
                } else {
                    try writer.writeAll("null");
                }
            },
            .error_union => {
                if (value) |payload| {
                    try formatDump(payload, writer);
                } else |err| {
                    try writer.print("{}", .{err});
                }
            },
            .pointer => |info| switch (info.size) {
                .one => try formatDump(value.*, writer),
                .slice => {
                    if (info.child == u8) {
                        try writer.print("{s}", .{value});
                    } else {
                        try writer.writeAll(".{ ");
                        for (value, 0..) |elem, i| {
                            if (i > 0) try writer.writeAll(", ");
                            try formatDump(elem, writer);
                        }
                        try writer.writeAll(" }");
                    }
                },
                else => try writer.print("{}", .{value}),
            },
            .array => |info| {
                if (info.child == u8) {
                    try writer.print("{s}", .{value});
                } else {
                    try writer.writeAll(".{ ");
                    for (value, 0..) |elem, i| {
                        if (i > 0) try writer.writeAll(", ");
                        try formatDump(elem, writer);
                    }
                    try writer.writeAll(" }");
                }
            },
            else => try writer.print("{}", .{value}),
        }
    }

    pub fn format(self: @This(), writer: *std.Io.Writer) std.Io.Writer.Error!void {
        return formatDump(self.data, writer);
    }
} {
    return .{ .data = val };
}

It doesn’t protect against any recursive types or stuff like that because I didn’t need it. There are likely also other things missing.

2 Likes

Why doesn’t it work on master anymore? I have very similar code

I think it is just, that some things in the structs in the typeinfo tagged union have changed. Likely nothing major.

I’ve just ran it through some relatively new compiler build I had on my machine(less than a week old) and it didn’t compile. I haven’t looked into it because I don’t need it right now.