Comptime error about alignment

I’m trying to create my own facade to the std.log.* functions that will treat a struct argument as a set of key-value pairs to be formatted as structured logging. In short, I want to transform the first snippet into the second snippet below:

const qux = 42;
mylog.debug("hello", .{.foo = "bar", .baz = qux});
const qux = 42;
std.log.debug("hello foo={s}, baz={d}\n", .{"bar, qux});

I thought I had this figured out, but now I’m getting an error about alignment.

/nix/store/gppcnnfah4li6smqvysxv8axyc2zbb4n-zig-0.15.2/lib/zig/std/meta.zig:942:35: error: use of undefined value here causes illegal behavior
            .alignment = @alignOf(T),
                                  ^
/nix/store/gppcnnfah4li6smqvysxv8axyc2zbb4n-zig-0.15.2/lib/zig/std/meta.zig:929:29: note: called at comptime here
    return CreateUniqueTuple(types.len, types[0..types.len].*);
           ~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/log.zig:130:26: note: called at comptime here
    return std.meta.Tuple(&types);
           ~~~~~~~~~~~~~~^~~~~~~~
src/log.zig:211:56: note: called at comptime here
        return .{ .fmt = &fmt_buf, .tuple_t = TupleType(args_type_info) };
                                              ~~~~~~~~~^~~~~~~~~~~~~~~~
src/log.zig:230:54: note: called at comptime here
    comptime var fmt_info = structuredLogFormatString(format, @TypeOf(args));

Below is the bulk of the comptime code that does this transformation. It’s kind of my first time writing comptime code so feel free to point out where things could be cleaner/simpler.

Here’s the general strategy:

  • Determine whether the format arguments are a struct or tuple struct
  • For a struct:
    • Loop over the struct fields to collect their types and the length of their portion of the transformed format strings
    • Create a buffer for the format string
    • Create a tuple with the correct types, initially filled with undefined
    • Loop over the struct fields again to fill out the buffer of the format string now that we have one.
  • For a tuple struct:
    • Similar, but simpler since we don’t need to format as key-value pairs, we just want to prefix with a timestamp (which the above also does).
fn structuredLogFormatString(comptime format: []const u8, comptime args_type: type) struct { fmt: []const u8, tuple_t: type } {
    const args_type_info = @typeInfo(args_type);
    if (args_type_info != .@"struct") {
        @compileError("expected tuple or struct argument, found " ++ @typeName(args_type));
    }

    const fields_info = args_type_info.@"struct".fields;
    const max_format_args = @typeInfo(std.fmt.ArgSetType).int.bits;
    if (fields_info.len > max_format_args) {
        @compileError("32 arguments max are supported per format call");
    }

    const ts_fragment = "{d}ms: ";
    comptime var fmt_buf_len: usize = 0;
    fmt_buf_len += ts_fragment.len;
    fmt_buf_len += format.len;

    const struct_info = args_type_info.@"struct";
    if (!struct_info.is_tuple) {
        // Convert to tuple form
        comptime var types: [struct_info.fields.len + 1]type = undefined;
        types[0] = i64; // timestamp
        inline for (1.., struct_info.fields) |i, field| {
            fmt_buf_len += 1 + field.name.len;
            switch (@typeInfo(field.type)) {
                .int => {
                    fmt_buf_len += "={d}".len;
                },
                .pointer => |ptr_t| {
                    if (isString(ptr_t)) {
                        fmt_buf_len += "=\"{s}\"".len;
                    } else {
                        fmt_buf_len += "={any}".len;
                    }
                },
                else => {
                    fmt_buf_len += "={any}".len;
                },
            }
            types[i] = field.type;
        }
        fmt_buf_len += 1; // newline
        comptime var fmt_buf: [fmt_buf_len]u8 = undefined;
        comptime var cursor: usize = 0;
        @memcpy(fmt_buf[cursor..(cursor + ts_fragment.len)], ts_fragment);
        cursor += ts_fragment.len;
        @memcpy(fmt_buf[cursor..(cursor + format.len)], format);

        inline for (1.., struct_info.fields) |i, field| {
            fmt_buf[cursor] = ' ';
            cursor += 1;

            const name = field.name;
            @memcpy(fmt_buf[cursor..(cursor + name.len)], name);
            cursor += name.len;

            switch (@typeInfo(field.type)) {
                .int => {
                    @memcpy(fmt_buf[cursor..(cursor + 4)], "={d}");
                    cursor += 4;
                },
                .pointer => |ptr_t| {
                    if (isString(ptr_t)) {
                        @memcpy(fmt_buf[cursor..(cursor + 6)], "=\"{s}\"");
                        cursor += 6;
                    } else {
                        @memcpy(fmt_buf[cursor..(cursor + 6)], "={any}");
                        cursor += 6;
                    }
                },
                else => {
                    @memcpy(fmt_buf[cursor..(cursor + 6)], "={any}");
                    cursor += 6;
                },
            }
            types[i] = field.type;
            fmt_buf[fmt_buf_len - 1] = '\n';
        }
        return .{ .fmt = &fmt_buf, .tuple_t = TupleType(args_type_info) };
    } else {
        comptime var types: [struct_info.fields.len + 1]type = undefined;
        types[0] = i64; // timestamp
        inline for (1.., struct_info.fields) |i, field| {
            types[i] = field.type;
        }
        fmt_buf_len += 1; // newline
        var fmt_buf: [fmt_buf_len]u8 = undefined;
        var cursor: usize = 0;
        @memcpy(fmt_buf[cursor..(cursor + ts_fragment.len)], ts_fragment);
        cursor += ts_fragment.len;
        @memcpy(fmt_buf[cursor..(cursor + format.len)], format);
        fmt_buf[fmt_buf_len - 1] = '\n';
        return .{ .fmt = &fmt_buf, .tuple_t = TupleType(args_type_info) };
    }
}

fn TupleType(comptime arg_type: std.builtin.Type) type {
    std.debug.assert(arg_type == .@"struct");
    const s = arg_type.@"struct";
    const len = s.fields.len + 1; // +1 for timestamp type
    var types: [len]type = undefined;
    inline for (1.., s.fields) |i, field| {
        types[i] = field.type;
    }
    return std.meta.Tuple(&types);
}

fn structured(comptime level: std.log.Level, comptime format: []const u8, args: anytype) void {
    comptime var fmt_info = structuredLogFormatString(format, @TypeOf(args));
    comptime var fmt_str = fmt_info.fmt;
    comptime var tuple_t = fmt_info.tuple_t;
    const ts = std.time.milliTimestamp();
    var values: fmt_info.tuple_t = undefined;
    values[0] = ts;
    inline for (1.., @typeInfo(@TypeOf(args)).@"struct".fields) |i, field| {
        values[i] = @field(args, field.name);
    }
    switch (level) {
        .debug => std.log.debug(fmt_str, args),
        .info => std.log.info(fmt_str, args),
        .warn => std.log.warn(fmt_str, args),
        .err => std.log.err(fmt_str, args),
    }
}

pub fn debug(comptime format: []const u8, args: anytype) void {
    structured(.debug, format, args);
}


you’re adding an extra slot for a timestamp, yet you don’t add it to the array. So that slot is left as undefined.

I am pleasantly surprised zig detected that, who knows what could have happened if it didnt :fearful:

2 Likes

Good catch!

Here’s the next issue:

src/log.zig:241:31: error: runtime value contains reference to comptime var
        .info => std.log.info(fmt_str, args),
                              ^~~~~~~
src/log.zig:241:31: note: comptime var pointers are not available at runtime
src/log.zig:176:49: note: 'runtime_value.ptr' points to comptime var declared here
        comptime var fmt_buf: [fmt_buf_len]u8 = undefined;

I declare fmt_buf_len as comptime var, so I think that’s good, but whether I initialize this array with undefined or .{0} ** fmt_buf_len I get this same error trying to return fmt_buf[0..] so I can use it as comptime const[] u8 in std.log.debug. If “comptime var pointers” aren’t available at runtime, is it even possible to slice a “string” at comptime unless you’re talking about a string literal?

You can do something like this:

const frozen = fmt_buf;
return .{ .fmt = &frozen, .tuple_t = TupleType(args_type_info) };

Can you explain why that works?

A comptime var is only allowed to be referenced in a comptime context.

While std.log.info does take a comptime know fmt string, comptime parameters can be referenced at runtime, so it is not allowed, unless the whole function is called at comptime.

const frozen = fmt_buf copies the data from the comptime var into a const, which can be used at runtime.
The comptime var can still be modified at comptime after that copy, but the const won’t observe those changes.

fyi, in fn structured I dont think any of those vars need to be comptime vars.
it will still compile if you dont change that as you copy from them, not referencing them.

2 Likes

Alright, I got this all working, but the developer experience isn’t great at the moment. Calls to my functions don’t show up in build errors, for example. Let’s say I have a bug in my implementation for formatting tuple structs, then try to build this small example:

const std = @import("std");
const slog = @import("slog");

pub const std_options: std.Options = .{
    .log_level = .debug,
};

pub fn main() !void {
    const x: usize = 42;
    slog.debug("aaaaa", .{x}); // triggers my bug
}

The error message you get looks like this, which doesn’t include any information about the call site:

❯ zig build
install
└─ install slog
   └─ compile exe slog Debug native 1 errors
/nix/store/h5zqw1lr8i0pwqpkkl3lbid06f9dprw0-zig-0.15.2/lib/zig/std/Io/Writer.zig:717:18: error: unused argument in 'debug: {d}ms: aaaaa

                                                                                                '
            1 => @compileError("unused argument in '" ++ fmt ++ "'"),
                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
referenced by:
    defaultLog__anon_19588: /nix/store/h5zqw1lr8i0pwqpkkl3lbid06f9dprw0-zig-0.15.2/lib/zig/std/log.zig:156:27
    log__anon_19516: /nix/store/h5zqw1lr8i0pwqpkkl3lbid06f9dprw0-zig-0.15.2/lib/zig/std/log.zig:124:22
    8 reference(s) hidden; use '-freference-trace=10' to see all references

Is there any way to include call site information?

and then look to see which one ends up in your code
Or you also can use -freference-trace then it uses the max

2 Likes