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.

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.

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.

Struct doesn’t have fields anymore. Change to field_names

        switch (@typeInfo(ValT)) {
            .@"struct" => |info| {
                try writer.writeAll(".{ ");
                inline for (info.field_names, 0..) |field_name, 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(" }");
            },

ah yes, that’s a trivial change. thanks