Union fields are empty

I was trying to create a graph data structure with Zig. Basically I pass in a union, that contains 1 or more structs as the union fields. Then for each struct of union I add two additional fields id and edges.

So if I pass in a Zig union like this:

const Package: type = union(enum) {
        Game: struct {
            title: []const u8,
            ry: usize,
        },
        User: struct {
            name: []const u8,
            age: usize,
        },
    };

I was expecting to get this:

    const Package: type = union(enum) {
        Game: struct {
            tag: Tag,
            edges: []*Edges,
            title: []const u8,
            ry: usize,
        },
        User: struct {
            tag: Tag,
            edges: []*Edge,
            name: []const u8,
            age: usize,
        },
    };

This is the whole code:

const std: type = @import("std");
const uuid: type = @import("uuid");
const Type: type = std.builtin.Type;

pub fn Graph(comptime T: type) type {
    if (@typeInfo(T) != .Union) @compileError("Invalid Schema");
    return comptime struct {
        allocator: std.mem.Allocator,
        nodes: []*Node,

        const Self = @This();

        pub const Node: type = @Type(.{ .Union = Type.Union{
            .decls = &[_]Type.Declaration{},
            .layout = .Auto,
            .tag_type = enum {},
            .fields = blk: {
                var union_fields: [std.meta.fields(T).len]Type.UnionField = undefined;
                for (std.meta.fields(T), 0..) |union_field, i| {
                    const add_fields: []const Type.StructField = &[_]Type.StructField{
                        .{ .type = uuid.urn, .name = "id", .alignment = 0, .is_comptime = false, .default_value = null },
                        .{ .type = []Edge, .name = "edges", .alignment = 0, .is_comptime = false, .default_value = null },
                    };

                    const new_struct: Type.Struct = .{
                        .fields = add_fields ++ std.meta.fields(union_field.type),
                        .decls = &[_]Type.Declaration{},
                        .layout = .Auto,
                        .is_tuple = false,
                    };
                    union_fields[i] = .{
                        .name = union_field.name,
                        .type = @Type(.{ .Struct = new_struct }),
                        .alignment = 0,
                    };
                }
                break :blk &union_fields;
            },
        } });

        pub const Edge: type = struct {
            label: []const u8,
            nodes: []*Node,
        };

        pub fn init(alloc: std.mem.Allocator) !*Self {
            var graph: *Self = try alloc.create(Self);
            graph.allocator = alloc;
            graph.nodes = &[0]*Node{};

            return graph;
        }

        pub fn deinit(self: *Self) void {
            self.allocator.destroy(self);
        }
    };
}

But when I did

std.debug.print("{}\n", .{std.meta.fields(Node).len});

I get 0;

I checked this snippet separately:

const fields: []const Type.UnionField = comptime blk: {
                var union_fields: [std.meta.fields(T).len]Type.UnionField = undefined;
                for (std.meta.fields(T), 0..) |union_field, i| {
                    const add_fields: []const Type.StructField = &[_]Type.StructField{
                        .{ .type = uuid.urn, .name = "id", .alignment = 0, .is_comptime = false, .default_value = null },
                        .{ .type = []Edge, .name = "edges", .alignment = 0, .is_comptime = false, .default_value = null },
                    };

                    const new_struct: Type.Struct = .{
                        .fields = add_fields ++ std.meta.fields(union_field.type),
                        .decls = &[_]Type.Declaration{},
                        .layout = .Auto,
                        .is_tuple = false,
                    };
                    union_fields[i] = .{
                        .name = union_field.name,
                        .type = @Type(.{ .Struct = new_struct }),
                        .alignment = 0,
                    };
                }
                break :blk &union_fields;
            },

And it works as expected. I have been looking for a solution for 2 days and I can’t any.

3 Likes

I think this is the problem. I guess this makes the compiler think that your union has no fields, since the tag has no fields. Changing it to the following seems to fix your example:

.tag_type = @typeInfo(T).Union.tag_type,
8 Likes

This appears to be an oversight in the compiler, the empty enum {} as a tag to a non-empty union should should have been a compile error that hopefully should have explained what was wrong in the code.

Quick minimal repro that shouldn’t compile but does on master:

const MyUnion = @Type(.{
    .Union = .{
        .layout = .Auto,
        .tag_type = enum {},
        .fields = &.{
            .{ .name = "a", .type = u32, .alignment = @alignOf(u32) },
            .{ .name = "b", .type = u32, .alignment = @alignOf(u32) },
        },
        .decls = &.{},
    },
});

pub fn main() void {
    const u: MyUnion = undefined;
    _ = u;
}

I opened a PR that fixes it. Good find!

3 Likes

Instead of using metaprogramming for this you should just simply wrap the passed in union in a struct containing the new fields, e.g.

const Package = struct {
    tag: Tag,
    edges: []*Edges,

    u: union(enum) {
        Game: struct {
            title: []const u8,
            ry: usize,
        },
        User: struct {
            name: []const u8,
            age: usize,
        },
    },
};

Presumably, OP wants to do this with more types than just what he showed in the example.

I can confirm that @IntegratedQuantum’s answer solves this problem. It also works with unions backed by explicit enums. Since I see that the OP has liked the answer, I’m going to mark @IntegratedQuantum’s answer as the solution to this thread. Likewise, it looks like @Castholm’s PR will help report this error in the future.

Please remember to mark Help threads as solved if the issue no longer persists :slight_smile:

1 Like

Sorry for the late reply. I tried the way @efjimm suggested, but I was not satisfied with the result. I wanted to do more and learn this language. So I tried to do it by metaprogramming. I also wanted to make it so that insted of the result coming out like this

.{
    .id = <UUID>,
    .edges = []*Edge{},
    .u = .Game {
        .title = "<UUID>",
        .ry = <Release_Year>,
    }
}

I wanted the data like this

.{
    .id = <UUID>
   .edges = []*Edges{},
   .title = "<Title>",
   .ry = <Release_Year>,
}

It felt more integrated and seamless, Also it fit the project for which I created this graph data structure. This method also looked way better in my opinion.