Programmatically adding a const decl to a struct type at comptime

i want to write a generic function that takes a struct S as input and returns a struct S' as output, where S' has the same fields as S but with an additional const decl that i’ll reflect upon later as a “marker trait”…

to simplify the problem, assume S only has fields and no other decls… the types S and S' can also be “anonymous” specified as struct { ... }

presumably @Type() is involved in the answer, but the details elude me at this time…

This is a lot harder than adding fields. I was playing around with this and most of the options aren’t great. You can easily add a comptime void field to a struct and then check if you have it.

Essentially, Declaration leaves you pretty much abandoned at this point:

    /// This data structure is used by the Zig language code generation and
    /// therefore must be kept in sync with the compiler implementation.
    pub const Declaration = struct {
        name: [:0]const u8,
    };

While StructField gives you what you want:

    /// This data structure is used by the Zig language code generation and
    /// therefore must be kept in sync with the compiler implementation.
    pub const StructField = struct {
        name: [:0]const u8,
        type: type,
        default_value: ?*const anyopaque,
        is_comptime: bool,
        alignment: comptime_int,
    };

And if you try to add declarations using @Type then you’ll get this error:

main.zig:20:12: error: reified structs must have no decls

The thing is, I can add a comptime field to my struct through reification:

const S = struct {
    x: usize = 42, // of course  
};

pub fn MakeImportant(comptime T: type) type {

    const fields = @typeInfo(T).Struct.fields;

    comptime var new_fields: [fields.len + 1]std.builtin.Type.StructField = undefined;

    for (fields, 0..) |field, i| {
        new_fields[i] = field;
    }

    new_fields[fields.len] = .{
        .name = "important",
        .type = void,
        .default_value = &void{},
        .is_comptime = true,
        .alignment = 0,
    };
    
    return @Type(.{ .Struct = .{
        .layout = .auto,
        .fields = new_fields[0..],
        .decls = &.{},
        .is_tuple = false,
        .backing_integer = null,
    } });
}

pub fn main() !void {
    const s: MakeImportant(S) = .{};
    std.debug.print("is important: {}", .{ @hasField(@TypeOf(s), "important")});
}

And in this case, the size of the struct is still 8 - the size of its only usize member variable.

3 Likes

I think @AndrewCodeDev is right that this isn’t easily possible, while I think the idea of using a zero sized field is valid and clever, it also feels a bit hacky to me.
If there is generic code processing all fields that may also need to explicitly ignore the comptime field.

Here is what I would do instead.
When S only has fields, you simply wrap S by making it a field in some other type.

const Wrapper = struct {
    s: S,

    pub const Important = true;
};

Then you can change your accessing code to access wrapper.s when @TypeOf(x) == Wrapper.
But because only having a wrapper sometimes makes the access asymmetrical and thus more difficult, having to check types via comptime, you could instead have 2 different wrappers:

const Normal = struct {
    s: S,
    pub const Important = false;
};
const Special = struct {
    s: S,
    pub const Important = true;
}

Now you can just always use wrapper.s to unwrap the value und use W.Important to know if the value needs to be treated differently.
If you have only one property you can just use the type of the wrapper instead of Important.

I think by the same token, we could just have a a function that has a comptime parameter type that also takes in a bool and returns another type:

pub fn AddStuff(comptime T: type, comptime stuff: bool) type {
    return struct {
        t: T,
        pub const stuff = stuff;
    };
}

That’s true about the extra field though - if you’re doing a lot of field processing, you could pick up that void field too. There isn’t a nice way to do this by my understanding.

2 Likes

the solution proposed by @AndrewCodeDev certainly meets my needs…

in a language where duck-typing is emphasized – and the actual @typeName of some type is not always obvious – this is a great pattern for encoding comptime-discoverable “traits” than can be either explicitly or programmatically added to a struct

my use-case will do lots of reflection; and these sorts of markers are relatively easy to spot using @typeInfo and @hasField

bottom line – we discover objects programmatically by matching a “well-known name”, which in zig corresponds to a field of that name rather than some named type…

Properly written code that deals with fields would check the is_comptime field before
operating on them, so I don’t think that’s huge issue. Comptime field is a proper language feature. I think Sze got the impression that your solution is hacky only because you chose to put a void in there instead of, say, an anonymous struct.

Maybe it is just a way of signaling that I haven’t encountered often yet. Maybe it feels less hacky as you get more used to it.

Personally I haven’t had a need for it yet, maybe I need to keep the pattern in mind and see where it would make sense, to get more used to it. I think one thing that makes it kind of rare is that because you can’t create your own declarations for types created with @Type, I tend to avoid mixing complexity with types created via @Type and instead put the complexity in a type that just wraps around it.

When you can’t really attach methods etc. to the created type it doesn’t seem that helpful to add marker const declarations to it, but maybe I just haven’t encountered particular uses of the feature.

So far my use of comptime fields was mostly as tuples. Not really mixing run-time fields with comptime fields a whole lot.

to your point, “reification” of structs can’t add decls anyway (as pointed out earlier in this thread)…

since my use of these marker fields is sometimes explicit and sometimes done programmatically, at least i’m consistent!!!

for the sort of very rich duck-typing i anticipate using, this technique allows me to perform an extra level of semantic checking beyond what i can overtly express in the base zig type system…

I tried this for padding a struct to a cache line. I had a wrapper:

pub fun Padded(comptime Inner: type) type {
  return extern struct { 
    // usingnamespace Inner; // tried this too
    const __pad_size = pad_to_amount(64, @sizeOf(Inner));
    d: Inner,
     __pad: [__pad_size]u8 = [_]u8{0} ** __pad_size,
    pub fn init_padded() @This() {
      return .{.d=Inner.init()};
    }
  };
}

and it was a disaster that I had to roll back. The code became so confusing. I tried adding usingnamespace Inner to it too, but still horrible. All the accessing code was var.d.insidevar.init().d.etc.... It make the code difficult to write and horrible to try to read. I went back to doing everything by hand. The old way of usingnamespace would have eliminated all of that. The wrapper struct get unmanageable so fast.

I wrote some comptime functions that check to see if one type is supertype of another where supertype is deifned as having all pub decls and fields of another but not just limited to those. I can then write an interface struct (yeah each function needs a body still but I make it just a simple return of whatever – it never gets run just needs to be there syntactically) and reference it in the documentation so show what fn signatures, decls, and fields need to be implemented. And the first line of the any function taking an anytype can do a full typechecl.

The hashmap has this very very long hand written highly specific verifyContext function. I’m not sure why that wasn’t just written generically and added to meta. Everybody with their own bespoke implementations is really bad for the langauge.

I added a const __implOf: type = InterfaceType so the interface checker can recursive check all types in consistent with their interfaces. There is a problem right now with mutually recursive types, and either use the void trick or just make comptime array of all the types I’ve seen and just keep adding to that and looking them up or some other way to stop the recursion. (I use __ as a prefix to items that signaling or autogenerated).