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:

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?