From eb2ea38e3478c00b8e9d12700f288769b0925ae4 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Thu, 17 Jul 2025 21:55:22 +0200 Subject: [PATCH] feat(pretty_print): handle union and pointer (including slices) --- README.md | 110 ++++++++++++++++++++++++++---------- src/main.zig | 45 ++++++++++++--- src/zlog.zig | 157 +++++++++++++++++++++++++++++++++++++-------------- 3 files changed, 234 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 3be0eb7..d914862 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,36 @@ # zlog -Standard Library log wrapper. `zlog` provides adjusted `std.log` output and a pretty print function for easy overwriting of user defined types (`struct`, `enum`, `union` and `vector`). +Standard Library log wrapper. `zlog` provides adjusted `std.log` output and a pretty print function for easy overwriting of user defined types. > [!CAUTION] > Only builds using the zig master version are tested to work. ## Usage -The following snippet shows an example usage of `zlog` to change the default log format and add pretty printing to both user defined types (`Options` and `Struct`): +The following snippet shows an example usage of `zlog` to change the default log format and add pretty printing to the user defined types: ```zig -const std = @import("std"); -const zlog = @import("zlog"); +const Union = union { + int: i32, + boolean: bool, -// use this to overwrite the default log format of `std.log` -pub const std_options = zlog.std_options; + // copy and paste this function into your user defined types to enable pretty printing for these types + pub fn format(value: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { + try zlog.pretty_format(value, fmt, options, writer); + } +}; + +const TaggedUnion = union(enum) { + one: i16, + two: u32, + three: []const u8, + nothing, + + // copy and paste this function into your user defined types to enable pretty printing for these types + pub fn format(value: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { + try zlog.pretty_format(value, fmt, options, writer); + } +}; const Options = enum { a, @@ -32,7 +48,8 @@ const Struct = struct { b: bool = true, c: [5]u16 = .{ 1, 2, 3, 4, 5 }, d: []const u8 = "string", - e: Options = Options.b, + e: Options = .b, + f: TaggedUnion = .{ .one = -5 }, // same function as above... pub fn format(value: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { @@ -43,15 +60,20 @@ const Struct = struct { pub fn main() void { // initialize zlog with the scope of `main` const log = std.log.scoped(.main); - // NOTE: the scope of `default` will result in no scoping being printed in - // the resulting output (default behavior of `std.log.defaultLog`) + // NOTE the scope of `default` will result in no scoping being printed in + // the resulting output (default behavior of `std.log.defaultLog`) // some variables to log const int_var = 42; const array_var = [_]i32{ 1, 2, 3, 4 }; const string_var = "This is a test"; - const option_var = Options.a; + const option_var: Options = .a; const struct_var: Struct = .{}; + const tagged_union: TaggedUnion = .{ + .three = "Three", + }; + const void_tagged_union: TaggedUnion = .nothing; + const uniun: Union = .{ .boolean = true }; // NOTE: Depending on the optimization target some of these log messages // will not show, which is inline with the behavior of `std.log`. @@ -59,32 +81,62 @@ pub fn main() void { log.info("Info message {any}", .{array_var}); log.info("Info message \"{s}\"", .{string_var}); log.warn("Warning message {any}", .{option_var}); + log.warn("Warning message {any}", .{void_tagged_union}); + log.warn("Warning message {any}", .{uniun}); log.err("Error message {any}", .{struct_var}); + log.err("Error message {any}", .{tagged_union}); } + +pub const std_options = zlog.std_options; + +const std = @import("std"); +const zlog = @import("zlog"); ``` This will result in the following output: ``` -[2025-02-24 13:00:08] [debug](main): Debug message 42 -[2025-02-24 13:00:08] [info](main): Info message { 1, 2, 3, 4 } -[2025-02-24 13:00:08] [info](main): Info message "This is a test" -[2025-02-24 13:00:08] [warning](main): Warning message main.Options = enum { - a, - b, - c, -} = a -[2025-02-24 13:00:08] [error](main): Error message main.Struct = struct { - .a = 42, - .b = true, - .c = []u16: { 1, 2, 3, 4, 5 }, - .d = { 115, 116, 114, 105, 110, 103 }, - .e = main.Options = enum { - a, - b, - c, - } = b, +[2025-07-17 19:41:43] [debug](main): Debug message 42 +[2025-07-17 19:41:43] [info](main): Info message { 1, 2, 3, 4 } +[2025-07-17 19:41:43] [info](main): Info message "This is a test" +[2025-07-17 19:41:43] [warning](main): Warning message main.Options = enum { + a, + b, + c, +} = .a +[2025-07-17 19:41:43] [warning](main): Warning message main.TaggedUnion = union(enum) { + one: i16, + two: u32, + three: []const u8, + nothing, +} = .{ .nothing = void } +[2025-07-17 19:41:43] [warning](main): Warning message main.Union = union { + int: i32, + boolean: bool, +} = @7fffd1c71048 +[2025-07-17 19:41:43] [error](main): Error message main.Struct = struct { + .a = 42, + .b = true, + .c = []u16: { 1, 2, 3, 4, 5 }, + .d = "string", + .e = main.Options = enum { + a, + b, + c, + } = .b, + .f = main.TaggedUnion = union(enum) { + one: i16, + two: u32, + three: []const u8, + nothing, + } = .{ .one = -5 }, } +[2025-07-17 19:41:43] [error](main): Error message main.TaggedUnion = union(enum) { + one: i16, + two: u32, + three: []const u8, + nothing, +} = .{ .three = "Three" } ``` ## Customization @@ -112,4 +164,4 @@ The following list shows some tips on how to use logging more effectively. These break :port try fmt.parseInt(u16, buf[0 .. len -| 1], 10); }; ``` -- When looking through the output of the log (i.e. written to disk by `program 2> log`) the `log` file contains control code characters (*ansi*) for the colored outputs. You can still see them correctly with the following command `less -rf log` +- When looking through the output of the log (i.e. written to disk by `program 2> log` when using the _stderr_ build option) the `log` file contains control code characters (*ansi*) for the colored outputs. You can still see them correctly with the following command `less -R log`. diff --git a/src/main.zig b/src/main.zig index 530fd13..c6637cb 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,7 +1,24 @@ -const std = @import("std"); -const zlog = @import("zlog"); +const Union = union { + int: i32, + boolean: bool, -pub const std_options = zlog.std_options; + // copy and paste this function into your user defined types to enable pretty printing for these types + pub fn format(value: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { + try zlog.pretty_format(value, fmt, options, writer); + } +}; + +const TaggedUnion = union(enum) { + one: i16, + two: u32, + three: []const u8, + nothing, + + // copy and paste this function into your user defined types to enable pretty printing for these types + pub fn format(value: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { + try zlog.pretty_format(value, fmt, options, writer); + } +}; const Options = enum { a, @@ -19,7 +36,8 @@ const Struct = struct { b: bool = true, c: [5]u16 = .{ 1, 2, 3, 4, 5 }, d: []const u8 = "string", - e: Options = Options.b, + e: Options = .b, + f: TaggedUnion = .{ .one = -5 }, // same function as above... pub fn format(value: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { @@ -30,15 +48,20 @@ const Struct = struct { pub fn main() void { // initialize zlog with the scope of `main` const log = std.log.scoped(.main); - // NOTE: the scope of `default` will result in no scoping being printed in - // the resulting output (default behavior of `std.log.defaultLog`) + // NOTE the scope of `default` will result in no scoping being printed in + // the resulting output (default behavior of `std.log.defaultLog`) // some variables to log const int_var = 42; const array_var = [_]i32{ 1, 2, 3, 4 }; const string_var = "This is a test"; - const option_var = Options.a; + const option_var: Options = .a; const struct_var: Struct = .{}; + const tagged_union: TaggedUnion = .{ + .three = "Three", + }; + const void_tagged_union: TaggedUnion = .nothing; + const uniun: Union = .{ .boolean = true }; // NOTE: Depending on the optimization target some of these log messages // will not show, which is inline with the behavior of `std.log`. @@ -46,5 +69,13 @@ pub fn main() void { log.info("Info message {any}", .{array_var}); log.info("Info message \"{s}\"", .{string_var}); log.warn("Warning message {any}", .{option_var}); + log.warn("Warning message {any}", .{void_tagged_union}); + log.warn("Warning message {any}", .{uniun}); log.err("Error message {any}", .{struct_var}); + log.err("Error message {any}", .{tagged_union}); } + +pub const std_options = zlog.std_options; + +const std = @import("std"); +const zlog = @import("zlog"); diff --git a/src/zlog.zig b/src/zlog.zig index e63485b..b036b26 100644 --- a/src/zlog.zig +++ b/src/zlog.zig @@ -1,23 +1,14 @@ -const build_options = @import("build_options"); -const std = @import("std"); -const ztime = if (build_options.timestamp) @import("ztime") else null; - -pub const std_options: std.Options = .{ - .logFn = logFn, -}; - -// zlog defaultLog function replacement, which adjusts the surrounding contents of every std.log message. +/// *zlog* defaultLog function replacement, which adjusts the surrounding contents of every `std.log` message. fn logFn( - comptime message_level: std.log.Level, + comptime message_level: log.Level, comptime scope: @Type(.enum_literal), comptime format: []const u8, args: anytype, ) void { - // TODO: provide build time configuration to allow tweaking corresponding output + // TODO provide build time configuration to allow tweaking corresponding output // - change output file for writing messages to (default `stderr`) - // write into own file for each level? + // - write into own file for each level? - // TODO: make the level text colored according to the level! const prefix = if (scope == .default) ": " else "(\x1b[2m" ++ @tagName(scope) ++ "\x1b[0m): "; const level_txt = switch (comptime message_level) { .err => "[\x1b[38;5;9merror\x1b[0m]", @@ -25,10 +16,10 @@ fn logFn( .info => "[\x1b[38;5;10minfo\x1b[0m]", .debug => "[\x1b[38;5;12mdebug\x1b[0m]", }; - const fmt = level_txt ++ prefix ++ format ++ "\n"; + const complete_format = level_txt ++ prefix ++ format ++ "\n"; if (comptime build_options.file.len != 0) { - // TODO: handle errors accordingly (i.e. panic?) - // NOTE: with zig 0.13.0 there is currently no way to open files to append (except to use libc or talk directly to posix, which this lib should not have to do) + // TODO handle errors accordingly (i.e. panic?) + // NOTE with zig 0.13.0 there is currently no way to open files to append (except to use libc or talk directly to posix, which this lib should not have to do) const file = std.fs.openFileAbsolute(build_options.file, .{ .mode = .read_write, }) catch std.fs.createFileAbsolute(build_options.file, .{ @@ -36,7 +27,7 @@ fn logFn( }) catch @panic("Failed to open and/or create configured log file"); defer file.close(); - var buffer = std.io.bufferedWriter(file.writer()); + var buffer = io.bufferedWriter(file.writer()); defer buffer.flush() catch {}; const writer = buffer.writer(); @@ -44,21 +35,21 @@ fn logFn( } if (comptime build_options.stderr) { - var buffer = std.io.bufferedWriter(std.io.getStdErr().writer()); + var buffer = io.bufferedWriter(io.getStdErr().writer()); defer buffer.flush() catch {}; std.debug.lockStdErr(); defer std.debug.unlockStdErr(); const writer = buffer.writer(); - log_writing(writer, fmt, args); + log_writing(writer, complete_format, args); } } -inline fn log_writing(writer: anytype, comptime fmt: []const u8, args: anytype) void { +inline fn log_writing(writer: anytype, comptime format: []const u8, args: anytype) void { nosuspend { if (build_options.timestamp) log_timestamp(writer); - writer.print(fmt, args) catch return; + writer.print(format, args) catch return; } } @@ -66,14 +57,14 @@ inline fn log_timestamp(writer: anytype) void { writer.print("[\x1b[1m{any}\x1b[0m] ", .{ztime.DateTime.now()}) catch return; } -pub fn pretty_format(object: anytype, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { - try inner_format(object, fmt, options, writer, 0); +pub fn pretty_format(object: anytype, comptime format: []const u8, options: fmt.FormatOptions, writer: anytype) !void { + try inner_format(object, format, options, writer, 0); } -fn inner_format(object: anytype, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype, comptime depth: u8) !void { - // TODO: here `std.meta` might be useful +fn inner_format(object: anytype, comptime format: []const u8, options: fmt.FormatOptions, writer: anytype, comptime depth: u8) !void { const Object = @TypeOf(object); const object_info = @typeInfo(Object); + switch (object_info) { .@"struct" => |s| { try writer.writeAll(@typeName(Object)); @@ -84,8 +75,8 @@ fn inner_format(object: anytype, comptime fmt: []const u8, options: std.fmt.Form try writer.writeAll(field.name); try writer.writeAll(" = "); // TODO check corresponding type and try to adapt fmt accordingly! - // TODO: pass along the already parsed formatting options too - try inner_format(@field(object, field.name), fmt, options, writer, depth + 1); + // TODO pass along the already parsed formatting options too + try inner_format(@field(object, field.name), format, options, writer, depth + 1); try writer.writeAll(",\n"); } try writer.writeAll("\t" ** depth); @@ -100,27 +91,109 @@ fn inner_format(object: anytype, comptime fmt: []const u8, options: std.fmt.Form try writer.writeAll(",\n"); } try writer.writeAll("\t" ** depth); - try writer.writeAll("} = "); + try writer.writeAll("} = ."); try writer.writeAll(@tagName(object)); }, - // TODO: implement prett_printing for other user defined types (`union` and `vector`) - // TODO: recognize []const u8 types and print them as strings - .array => |a| { - if (a.child == @TypeOf([:0]const u8)) { - try std.fmt.format(writer, "\"{s}\"", .{object}); + .@"union" => |u| { + try writer.writeAll(@typeName(Object)); + try writer.writeAll(" = union"); + if (u.tag_type) |Tag| { + _ = Tag; + try writer.writeAll("(enum)"); + } + try writer.writeAll(" {\n"); + inline for (u.fields) |field| { + try writer.writeAll("\t" ** (depth + 1)); + try writer.writeAll(field.name); + if (@typeInfo(field.type) != .void) { + try writer.writeAll(": "); + try writer.writeAll(@typeName(field.type)); + } + try writer.writeAll(",\n"); + } + try writer.writeAll("\t" ** depth); + try writer.writeAll("} = "); + if (u.tag_type) |Tag| { + try writer.writeAll(".{ ."); + try writer.writeAll(@tagName(object)); + try writer.writeAll(" = "); + inline for (u.fields) |u_field| if (object == @field(Tag, u_field.name)) { + try inner_format(@field(object, u_field.name), format, options, writer, depth + 1); + }; + try writer.writeAll(" }"); } else { - try std.fmt.format(writer, "[]{s}: {any}", .{ @typeName(a.child), object }); + // NOTE the value of a union (untagged) is displayed like this also in the standard library, + // not sure if you can reflect the used variant (and its value) + try fmt.format(writer, "@{x}", .{@intFromPtr(&object)}); } }, - .vector => |v| { - if (v.child == @TypeOf([:0]const u8)) { - try std.fmt.format(writer, "\"{s}\"", .{object}); - } else { - try std.fmt.format(writer, "[]{s}: {any}", .{ @typeName(v.child), object }); - } + .pointer => |p| switch (p.size) { + .slice => switch (@typeInfo(p.child)) { + .int => |num| { + if (num.signedness != .unsigned) { + try fmt.format(writer, "[]{s}: {any}", .{ @typeName(p.child), object }); + } else { + switch (num.bits) { + 8 => try fmt.format(writer, "\"{s}\"", .{object}), + else => try fmt.format(writer, "[]{s}: {any}", .{ @typeName(p.child), object }), + } + } + }, + else => try fmt.format(writer, "[]{s}: {any}", .{ @typeName(p.child), object }), + }, + .c => switch (@typeInfo(p.child)) { + .int => |num| { + if (num.signedness != .unsigned) { + try fmt.format(writer, "[*c]{s}: {any}", .{ @typeName(p.child), object }); + } else { + switch (num.bits) { + 8 => try fmt.format(writer, "\"{s}\"", .{object}), + else => try fmt.format(writer, "[*c]{s}: {any}", .{ @typeName(p.child), object }), + } + } + }, + else => try fmt.format(writer, "[]{s}: {any}", .{ @typeName(p.child), object }), + }, + .many => try fmt.format(writer, "[*]{s}: {any}", .{ @typeName(p.child), object }), + .one => try fmt.format(writer, "[1]{s}: {any}", .{ @typeName(p.child), object }), }, - else => { - try std.fmt.format(writer, "{any}", .{object}); + .array => |a| switch (@typeInfo(a.child)) { + .int => |num| { + if (num.signedness != .unsigned) { + try fmt.format(writer, "[]{s}: {any}", .{ @typeName(a.child), object }); + } else { + switch (num.bits) { + 8 => try fmt.format(writer, "\"{s}\"", .{object}), + else => try fmt.format(writer, "[]{s}: {any}", .{ @typeName(a.child), object }), + } + } + }, + else => try fmt.format(writer, "[]{s}: {any}", .{ @typeName(a.child), object }), }, + .vector => |v| switch (@typeInfo(v.child)) { + .int => |num| { + if (num.signedness != .unsigned) { + try fmt.format(writer, "[]{s}: {any}", .{ @typeName(v.child), object }); + } else { + switch (num.bits) { + 8 => try fmt.format(writer, "\"{s}\"", .{object}), + else => try fmt.format(writer, "[]{s}: {any}", .{ @typeName(v.child), object }), + } + } + }, + else => try fmt.format(writer, "[]{s}: {any}", .{ @typeName(v.child), object }), + }, + else => try fmt.format(writer, "{any}", .{object}), } } + +pub const std_options: std.Options = .{ + .logFn = logFn, +}; + +const std = @import("std"); +const io = std.io; +const log = std.log; +const fmt = std.fmt; +const build_options = @import("build_options"); +const ztime = if (build_options.timestamp) @import("ztime") else null;