How to create functions/declarations for struct/union/enum at compile time?

Hello everyone, I’m having some trouble figuring out how to create (if possible) a declaration for a struct/union/enum using comptime and metaprogramming. In other words:
How to, programmatically, create a function/declaration that translates to:

const Foo = struct {
  pub fn bar() void {}
}

I’ve found this post asking a similar (maybe the same) question, that suggests wrapping the struct/union/enum in another struct/union/enum. Is this the only solution?

My use case is the following:

I’m initializing a enum (Tokens) and a StaticStringMap (TokensMap) using a const variable (_tokens), so I can add/remove fields from one place and have the enum and the StringMap automatically updated.

const _tokens = [_][2][:0]const u8{
    // (K,V) pairs
    .{ "+", "plus" },
    .{ "-", "minus" },
};

const Tokens = b: {
    var enum_fields: [_tokens.len]std.builtin.Type.EnumField = undefined;
    var i: u8 = 0;
    for (&enum_fields, _tokens) |*enum_field, token| {
        enum_field.* = .{ .name = token[1], .value = i };
        i += 1;
    }
    break :b @Type(.{ .Enum = .{
        .decls = &.{},
        .fields = &enum_fields,
        .tag_type = u8,
        .is_exhaustive = false,
    } });
};

const TokensMap = b: {
    var map_kvs: [_tokens.len]struct { [:0]const u8, Tokens } = undefined;
    var i: u8 = 0;
    for (&map_kvs, _tokens) |*map_kv, token| {
        map_kv.* = .{ token[0], @enumFromInt(i) };
        i += 1;
    }
    break :b std.StaticStringMap(Tokens).initComptime(map_kvs);
};

The code behaves as intended, but I want _tokens and TokensMap only present in the enum (Tokens) scope, and expose functions from the enum itself, so instead of calling TokensMap.has() I can call Tokens.has(), for example.

Probably, It seems that it cannot append function declarations dynamically.

As the alternative idea, you can implemented as file scope unit.

For example:

// Tokens.zig

const std = @import("std");

const _tokens = [_][2][:0]const u8{
    // (K,V) pairs
    .{ "+", "plus" },
    .{ "-", "minus" },
};

pub const Tag = b: {
    var enum_fields: [_tokens.len]std.builtin.Type.EnumField = undefined;
    var i: u8 = 0;
    for (&enum_fields, _tokens) |*enum_field, token| {
        enum_field.* = .{ .name = token[1], .value = i };
        i += 1;
    }
    break :b @Type(.{ .Enum = .{
        .decls = &.{},
        .fields = &enum_fields,
        .tag_type = u8,
        .is_exhaustive = false,
    } });
};

const TokensMap = b: {
    var map_kvs: [_tokens.len]struct { [:0]const u8, Tag } = undefined;
    var i: u8 = 0;
    for (&map_kvs, _tokens) |*map_kv, token| {
        map_kv.* = .{ token[0], @enumFromInt(i) };
        i += 1;
    }
    break :b std.StaticStringMap(Tag).initComptime(map_kvs);
};

pub fn has(op: []const u8) bool {
    return TokensMap.has(op);
}

In caller:

const std = @import("std");
const Tokens = @import("./Tokens.zig");

pub fn main() !void {
    std.debug.print("`+`: {}\n", .{Tokens.has("+")});
    std.debug.print("`/`: {}\n", .{Tokens.has("/")});
}

Result:

`+`: true
`/`: false
2 Likes

Thanks for your response, it’s a good solution, and since all Zig source files are structs the solution is “to wrap into another struct”, haha.

After making the post I found more information related to the current state of declarations and @Type(), apparently the main ones are #6709 and #10710.

To end it all, I learned a new verb.
Reify: To make (something abstract) more concrete or real, as in “error: reified structs must have no decls”.

1 Like