Getting the type associated to a tag in a tagged union

Let’s say you have this type:

const Data = union(enum) {
        num: u8,
        bool: bool,
};

const d = Data{.num = 23};

I know I can find the active tag with std.meta.activeTag(d), but how can I find the type associated to that tag ?

EDIT: so far my lead is @FieldType(Data, "num"), but I would have to transform the Tag type into a string representation.

1 Like

Use a switch with inline else:

switch (d) {
    inline else => |value| {
        const T = @TypeOf(value);
        // do something with T
    },
}

Note that it’s not possible to store the type of the active field in a variable in the outermost scope (unless d is comptime-known), you can only interact with it inside the prong. The way inline switch prongs work is that they duplicate the prong for every possible value.

3 Likes

Adding to @castholm 's answer, if d is always comptime-known @FieldType is what you want here. You can use @tagName to get the string representation of an enum/tagged union tag value:

const d: Data = .{ .num = 23 };
const T = @FieldType(Data, @tagName(d));
comptime @import("std").debug.assert(T == u8);
3 Likes
const DataTag = @typeInfo(Data).@"union".tag_type.?;
const tag: DataTag = Data{ .num = 1 };
std.debug.print("{}\n", .{tag});

This is just std.meta.activeTag

It’s not, the last 2 lines are just for demonstration, the 1st line already provides what OP wants, the enum of the tagged union.

Oh I though OP wanted to find the type of the union field associated with a tag value and not the type of the enum tag itself, but I might have misunderstood the original question (anyway both questions were answered in this thread :slight_smile: )

yeah, I may have misunderstood the question myself.

Thanks! @FieldType did the trick - but it makes me wonder if my solution is a bit convoluted.

Here is my use case: I’m building a simple stack based on an array of union(enum) so I can store multiple types of data in it. The first version was very simple and straightforward: push and pop methods are taking/returning the union type.

const std = @import("std");

const Stack = struct {
    array: [32]Data,
    length: usize,

    const init = Stack{
        .array = undefined,
        .length = 0,
    };

    const Data = union(enum) {
        num: u8,
        bool: bool,
    };

    fn push(s: *Stack, d: Data) !void{ ... }
    fn pop(s: *Stack) !Data{ ... }
    fn peek(s: *Stack) !Data{ ... }
}

It works, but it quickly gets verbose because every time I want to use something from the stack I have to pop and handle a possible underflow, and then unpack the data and handle a possible type mismatch.

To make it more concise, I wanted to have push/pop methods that packs/unpacks the data directly based on a tag argument - hence my question. The result is this:

const std = @import("std");

const Stack = struct {
    array: [32]Data,
    length: usize,

    const init = Stack{
        .array = undefined,
        .length = 0,
    };

    const Data = union(enum) {
        num: u8,
        bool: bool,
    };

    const Tag = std.meta.Tag(Data);

    fn TagType(comptime tag: Tag) type {
        return @FieldType(Data, @tagName(tag));
    }

    fn push(s: *Stack, comptime tag: Tag, value: TagType(tag)) !void {
        if (s.length == s.array.len) {
            return error.StackOverflow;
        }
        s.array[s.length] = @unionInit(Data, @tagName(tag), value);
        s.length += 1;
    }

    fn pop(s: *Stack, comptime tag: Tag) !TagType(tag) {
        if (s.length == 0) {
            return error.StackUnderflow;
        }
        const data = s.array[s.length - 1];
        defer s.length -= 1;

        if (std.meta.activeTag(data) != tag) {
            return error.InvalidType;
        }

        return @field(data, @tagName(tag));
    }

    fn print(s: *Stack) void {
        for (0..s.length) |i| {
            std.debug.print("{any}\n", .{s.array[i]});
        }
    }
};

test "push pop ok" {
    var s = Stack.init;
    try s.push(.num, 23);
    const v = try s.pop(.num);
    try std.testing.expect(v == 23);
}

test "invalid type" {
    var s = Stack.init;
    try s.push(.num, 23);
    try std.testing.expectError(error.InvalidType, s.pop(.bool));
}

test "underflow" {
    var s = Stack.init;
    try std.testing.expectError(error.StackUnderflow, s.pop(.bool));
}

Does it makes sense ? Am I bending backwards to solve this ?

1 Like

I would separate the Stack and Data and then compose them creating a UnionStack:

const std = @import("std");

pub fn Stack(comptime T: type, comptime n: usize) type {
    return struct {
        const Self = @This();

        array: [n]T,
        length: usize,

        const empty: Self = .{
            .array = undefined,
            .length = 0,
        };

        fn push(s: *Self, value: T) !void {
            if (s.length == s.array.len) return error.StackOverflow;
            s.array[s.length] = value;
            s.length += 1;
        }

        fn pop(s: *Self) !T {
            if (s.length == 0) return error.StackUnderflow;
            s.length -= 1;
            return s.array[s.length];
        }

        fn print(s: *Self) void {
            for (s.array[0..s.length]) |v| std.debug.print("{any}\n", .{v});
        }
    };
}

const Data = union(enum) {
    num: u8,
    bool: bool,
};

const UnionStack = struct {
    stack: Stack(Data, 32),

    const empty: UnionStack = .{ .stack = .empty };

    const Tag = std.meta.Tag(Data);

    fn TagType(comptime tag: Tag) type {
        return @FieldType(Data, @tagName(tag));
    }

    fn push(s: *UnionStack, comptime tag: Tag, value: TagType(tag)) !void {
        try s.stack.push(@unionInit(Data, @tagName(tag), value));
    }

    fn pop(s: *UnionStack, comptime tag: Tag) !TagType(tag) {
        const data = try s.stack.pop();
        switch (data) {
            inline else => |val, active_tag| {
                if (tag != active_tag) return error.InvalidType;
                return val;
            },
        }
    }

    fn print(s: *UnionStack) void {
        s.stack.print();
    }
};

test "push pop ok" {
    var s: UnionStack = .empty;
    try s.push(.num, 23);
    const v = try s.pop(.num);
    try std.testing.expect(v == 23);
}

test "invalid type" {
    var s: UnionStack = .empty;
    try s.push(.num, 23);
    try std.testing.expectError(error.InvalidType, s.pop(.bool));
}

test "underflow" {
    var s: UnionStack = .empty;
    try std.testing.expectError(error.StackUnderflow, s.pop(.bool));
}
2 Likes