Trying to add a new field to a struct at comptime

I’m trrying to add a new bool to a struct definition. I have a global option that turns on or off some feature, but I want it to be more granular. So I was going to add bools to a struct and then test if it exists and if it does it can override the global value. This is turn on and off various runtime debugging info that has been added at build time so the caller (the dev building) cannot specify the full set that is spread across many files.

pub const TagMap = struct {};

fn make_tag(comptime name: [:0]const u8) void {
    comptime {
        const SF = std.builtin.Type.StructField;
        var ti = @typeInfo(TagMap).Struct;
        const field = [1]SF{.{ .name = name, .type = ?bool, .default_value = null, .is_comptime = true, .alignment = 1 }};
        ti.fields = ti.fields ++ field;
        const new_ti = std.builtin.Type{ .Struct = ti };
        const new_type = @Type(new_ti);
        TagMap = @Type(new_type);
    }
}

test "asdf" {
    std.debug.print("\n", .{});
    comptime var fs = @typeInfo(TagMap).Struct.fields;
    std.debug.print("{d} {any}\n", .{ fs.len, fs });
    make_tag("asdf");
    fs = @typeInfo(TagMap).Struct.fields;
    std.debug.print("{d} {any}\n", .{ fs.len, fs });
}

You cannot change the type of const TagMap.
You can create a new type:

const std = @import("std");

pub const TagMap = struct {};

fn add_field(comptime T: type, comptime name: [:0]const u8) type {
    const ti = @typeInfo(T).Struct;
    comptime var fields: [ti.fields.len + 1]std.builtin.Type.StructField = undefined;
    comptime var i: usize = 0;
    inline while (i < ti.fields.len) {
        fields[i] = ti.fields[i];
        i += 1;
    }
    fields[ti.fields.len] = std.builtin.Type.StructField{
        .name = name,
        .type = ?bool,
        .default_value = null,
        .is_comptime = false,
        .alignment = 1,
    };
    return @Type(.{ .Struct = std.builtin.Type.Struct{
        .layout = ti.layout,
        .backing_integer = ti.backing_integer,
        .fields = &fields,
        .decls = ti.decls,
        .is_tuple = ti.is_tuple,
    } });
}

test "asdf" {
    const T = add_field(TagMap, "asdf");
    std.debug.print("{any}\n", .{@typeInfo(T).Struct.fields[0]});
}
1 Like

I wanted to keep adding fields and then I could use @hasField to detech if an option was active.

is there no way to do a comptime var and converts to a const at the end of comptime?

Can I append to a const array at comptime, or maybe I can set elements of a static array and just impose a cap on the number of tags?

I don’t have a good solution to offer. All I can think of is a shell script that scans every Zig file for an export under a particular name, then generates a Zig file that imports that symbol from all matching files, merging them into a feature set that you can then use in your build file.

Instead of making the field be there or not there, I think it would be way easier to:

  • always have the field
  • have a specific declaration define the type for it
  • toggle the type based on some comptime value
pub const Debug = if(comptime is_debug_mode) bool else void;
pub const DebugDefault = if(comptime is_debug_mode) true else {};

pub const DebuggableInstance = struct {
  some_field:u32,
  debug:Debug = DebugDefault,
};

Instead of void other zero sized types may be interesting in some circumstances:

pub const AlwaysDebug = struct {
   comptime enabled: bool = true,
};
pub const NeverDebug = struct {
   comptime enabled: bool = false,
};
pub const MaybeDebug = struct {
   enabled: bool = DefaultDebug,
};
pub const DebugMode = enum { always, never, maybe };
pub const Debug = switch(build_option_debug_mode) {
   .always => AlwaysDebug,
   .never => NeverDebug,
   .maybe => MaybeDebug,
};
pub const OtherDebuggableInstance = struct {
  some_field:u32,
  mode:Debug = .{},
};
pub fn setDebug(val:anytype, debug:bool) void {
    if(comptime @hasField(@TypeOf(val), "mode") and @TypeOf(val.mode) == MaybeDebug) {
        val.mode.enabled = debug;
    }
}

The first 2 have size zero but still can be read like normal fields, you just have to make sure that you guard things with if(comptime ...) checks so you don’t attempt to write to the comptime fields (at least you can’t set a different value than they already have, setting the same value is a no-op), other then that this allows you to switch between these different types and the type determines whether it will need space and whether its value can be changed at run-time.


tl;dr Instead of making the field optional, it is easier to switch between a zero sized type and a non-zero sized type.

the list of tags isn’t known upfront: here is was I currently do with something similar to turn assertions on and off in release modes. I @import the asserts struct from a project file, but requires some manual work each time (ie, I can’t just make it a library and the file has to exist too so each new project I use this in has to create the Assert struct with the nassert decl – I can’t find a way to basically mimic #if !defined(NASSERT) && !defined(ASSERT_GROUP_ASD) ...

const std = @import("std");
const meta = std.meta;

const Asserts = struct {
    const nassert: bool = false;
    asd: bool = true,
};

fn get_tag(comptime name: []const u8) ?bool {
    comptime {
        if (meta.fieldIndex(Asserts, name)) |i| {
            const f = meta.fields(Asserts)[i];
            const v = f.default_value;
            return if (v) |vv| @as(*const bool, @ptrCast(vv)).* else null;
        } else {
            return null;
        }
    }
}

fn assert(cond: bool) void {
    if (comptime !Asserts.nasset) {
        if (!cond)
            @panic("assertion failed");
    }
}

fn tassert(comptime tag: [:0]const u8, cond: bool) void {
    if (comptime get_tag(tag)) |t| {
        if (comptime t) {
            if (!cond)
                @panic("assertion failed tag=" ++ tag);
        }
    } else {
        if (comptime !Asserts.nassert) {
            if (!cond)
                @panic("assertion failed tag=" ++ tag);
        }
    }
}

const pp = std.debug.print;
pub fn main() void {
    pp("{?}\n", .{tassert("asd", true)});
    pp("{?}\n", .{tassert("asd", false)});
    pp("{?}\n", .{tassert("qwe", true)});
    pp("{?}\n", .{tassert("qwe", false)});
}