Control float/double formatting with `{any}`

When trying to debug-print structs, like you would with {:?} in Rust, how to control the formatting of float/doubles when using {any}?:

const std = @import("std");

const Foo = struct {
    bar: f32,
};

pub fn main() void {
    const foo = Foo { .bar = 0.123 };
    std.log.debug("foo {any}", .{ foo });
}

For example, trying to use decimal formatting instead of scientific ?

{any} will output a value of any type using a default format, as per fmt doc. To format a numeric value as a decimal use {d}. You can add width and precision: {d:6.2}.

So I take from your post that the answer to the question is that this is not possible to configure the formatting of float/double when using {any}.

As explained here:

You can add a format function to your type Foo or you could use the std.fmt.Formatter and use a freestanding function to explicitly format an argument to a printing function.

Yes, thanks for that I forgot to mention it, but I mainly use {any} for debug purposes (the way I use Debug and {:?} in rust) and I can’t read scientific notation due to my smol brain.

Having to write a format function to any random struct I want to debug is just not practical.

I though that zig would have something equivalent to std::fixed to globally configure the way floats are formatted.

You can add a format function to your type Foo or you could use the std.fmt.Formatter and use a freestanding function to explicitly format an argument to a printing function.

Ok I didn’t know this. So you could theorically re-implement {any} with your own generic Formatter and use it to debug-print any struct with the formatting you want.

I tried this:

fn formatDebugCtr(comptime T: type) (fn (T, []const u8, std.fmt.FormatOptions, anytype) anyerror!void) {
  return struct {
    fn formatDebug(
      any: T,
      comptime fmt: []const u8,
      options: std.fmt.FormatOptions,
      writer: anytype,
    ) !void {
      // TODO: set options to decimal?
      try std.fmt.formatType(any, fmt, options, writer, std.options.fmt_max_depth);
    }
  }.formatDebug;
}

pub fn fmtDebug(any: anytype) std.fmt.Formatter(formatDebugCtr(@TypeOf(any))) {
  return .{ .data = any };
}

But:

  1. I realize that FormatOptions does not let you specify the float format (scientific/decimal) only the precision.
  2. This crashes the compiler with zig compiler bug: GenericPoison.

So I’am out of options for a reasonably simple solution unfortunately.

1 Like

You can just do try writer.print("{d}", .{value}); within formatDebug if T is a number type. You can just ignore the format options if you want to.

Also you could write your own wrapper around std.debug.print or whatever you are using and automatically apply that formatting overwrite to any of the tuple arguments which are numbers.

For composite types it is more complicated and maybe too much work.

You also could maybe locally hack your std library and just change your default formatting. Maybe you could even copy the standard library formatting code and change it and use it as a custom library, but I don’t know whether it would be relatively straightforward or not.

Regarding 2. I am not entirely sure but maybe you are creating an endless loop, because I guess formatTypes with the same arguments just calls the formatDebug function again.


Hmm I guess you are calling it on the inner type, so it should use the default formatting? I am not sure what is going on there. What zig version are you running, do you have a small complete example that creates the compiler bug?

You can just do try writer.print("{d}", .{value}); within formatDebug if T is a number type. You can just ignore the format options if you want to.

Also you could write your own wrapper around std.debug.print or whatever you are using and automatically apply that formatting overwrite to any of the tuple arguments which are numbers.

Ok so from @dude_the_builder’s and this statement I realize that I wasn’t clear in my initial question. I have updated it with, hopefully, a clearer intent.

I am trying to debug print arbitrary complex structs which will contain some float fields (among many other fields which might be struct themselves) and I wanted to use decimal notation for those float fields.

For composite types it is more complicated and maybe too much work.

Yes that’s my point. Impossible to do today without a significant effort.

You also could maybe locally hack your std library and just change your default formatting. Maybe you could even copy the standard library formatting code and change it and use it as a custom library, but I don’t know whether it would be relatively straightforward or not.

Hacking it wouldn’t but having to keep it up to date with upstream zig would.

Hmm I guess you are calling it on the inner type, so it should use the default formatting? I am not sure what is going on there. What zig version are you running, do you have a small complete example that creates the compiler bug?

$ zig version
0.12.0-dev.3533+e5d900268

MRE:

const std = @import("std");

fn formatDebugCtr(comptime T: type) (fn (T, []const u8, std.fmt.FormatOptions, anytype) anyerror!void) {
  return struct {
    fn formatDebug(
      any: T,
      comptime fmt: []const u8,
      options: std.fmt.FormatOptions,
      writer: anytype,
    ) !void {
      // TODO: set options to decimal?
      try std.fmt.formatType(any, fmt, options, writer, std.options.fmt_max_depth);
    }
  }.formatDebug;
}

pub fn fmtDebug(any: anytype) std.fmt.Formatter(formatDebugCtr(@TypeOf(any))) {
  return .{ .data = any };
}

const MyStruct = struct {
  foo: f32,
};

pub fn main() !void {
  const myStruct: MyStruct = .{ .foo = 0.123 };
  std.log.debug("{}", .{ fmtDebug(myStruct) });
}

compile it with:

$ zig build-exe mre.zig
thread 41603 panic: zig compiler bug: GenericPoison
Unable to dump stack trace: debug info stripped
Aborted (core dumped)

Regarding the compiler bug I don’t know whether this is meant to be something that isn’t allowed and should get some kind of error message, about going a few levels too deep into meta-programming or something along those lines, or whether this is something that actually should work in zig.

It would be good to get a perspective on this compiler bug from somebody who knows what is going on. @mlugg Can you give us some perspective on what is happening here, if this is something that needs to be fixed / has an issue or needs one?


I can change the code to mimic what Formatter does and then it works (I think because it avoids using the “generically instanced” function and instead generates a type with a format function):

const std = @import("std");

fn FormatIt(comptime T: type) type {
    return struct {
        data: T,

        pub fn format(
            self: @This(),
            comptime fmt: []const u8,
            options: std.fmt.FormatOptions,
            writer: anytype,
        ) !void {
            try std.fmt.formatType(self.data, fmt, options, writer, std.options.fmt_max_depth);
        }
    };
}

fn formatIt(it: anytype) FormatIt(@TypeOf(it)) {
    return .{ .data = it };
}

const MyStruct = struct {
    foo: f32,
};

pub fn main() !void {
    const myStruct: MyStruct = .{ .foo = 0.123 };
    std.log.debug("{}", .{formatIt(myStruct)});
}

Output:

debug: mre2.MyStruct{ .foo = 1.23000003e-01 }

The part that is still unsolved is how could you customize the default way of printing nested values.

Maybe there could be something similar to a custom logger but for formatting defaults, which would allow you to pick which formatting is used when no explicit one is provided, but I don’t know if something like this has been proposed/rejected already.

1 Like

I think it is the return type on formatDebugCtr that is causing the compiler to have a headache here. I’m not knowledgeable on compiler internals and I haven’t been using Zig recently, but I remember running into similar issues when playing around with generic madness in the past.

Switching to the following compiles without issue.

const std = @import("std");

fn FormatDebugCtr(comptime T: type) type {
  return struct {
    fn formatDebug(
      any: T,
      comptime fmt: []const u8,
      options: std.fmt.FormatOptions,
      writer: anytype,
    ) !void {
      // TODO: set options to decimal?
      try std.fmt.formatType(any, fmt, options, writer, std.options.fmt_max_depth);
    }
  };
}

pub fn fmtDebug(any: anytype) std.fmt.Formatter(FormatDebugCtr(@TypeOf(any)).formatDebug) {
  return .{ .data = any };
}

const MyStruct = struct {
  foo: f32,
};

pub fn main() !void {
  const myStruct: MyStruct = .{ .foo = 0.123 };
  std.log.debug("{}", .{ fmtDebug(myStruct) });
}
2 Likes

Oh man, generic poison leaks are always annoying. This might be one of these issues, but I couldn’t be sure without looking into it more deeply. If you have a simple repro (it looks like you do), I’d suggest opening an issue; if it is the same as another issue we’ll catch it and close both.

3 Likes

I think this issue already tracks the same problem.

At least it crashes at the same stack trace in gdb. I would err on the side of not adding more noise to the issue tracker. Let me know if you thing otherwise.