Why @tagName "unable to evaluate comptime expression" inside a function with `anytype`?

I’m trying to print a tagged union field inside a function:

const std = @import("std");

pub fn main() !void {
    const Node = union(enum) { f1: u8, f2: u8 };

    const node = Node{ .f1 = 255 };
    prettyPrint(node);
}

fn prettyPrint(node: anytype) void {
    const node_T_info = @typeInfo(@TypeOf(node));
    if (node_T_info == .Union) {
        const tag_type = node_T_info.Union.tag_type;
        if (tag_type != null) {
            std.log.debug("{s}", .{@tagName(node)});
            std.log.debug("{any}", .{@field(node, @tagName(node))});
        }
    }
}

But running the above gives:

src/q_printUnion.zig:17:60: error: unable to evaluate comptime expression
            std.log.debug("{any}", .{@field(node, @tagName(node))});
                                                           ^~~~

However, if you take the content of the function and put it directly instead of prettyPrint(node), you’ll get:

debug: f1
debug: 255

Why?

Ok, if I put comptime in front of the node: anytype, it starts working. However, in the original version of this function (it was reduced for the sake of this question), I was able to pass a runtime value and still get the printing. Here:

pub fn prettyPrint(self: *Self, next: anytype) anyerror!void {
    const next_T_info = @typeInfo(next_T);
    switch (next_T_info) {
        .Pointer => {
            try self.out.appendSlice("*");
            if (next_T_info.Pointer.size == .Slice) {
                // recursive call with next.ptr
            } else if (next_T_info.Pointer.size == .One) {
                // recursive call with next.*
            }
        },
        .Int, .ComptimeInt, .Float, .ComptimeFloat, .Bool, .Type => {
            // ...
            printed = try std.fmt.bufPrint(&buf, "{any}", .{next});
            try self.out.appendSlice(printed);
            try self.out.append('\n');
        },
        // ...
}

pub fn main() !void {
   var f: f64 = 1.1;
   f = std.math.ceil(f); // this line is just to simulate an "external" change
   try prettyPrint(alloc, &f);
   // ...
}

Result:

*f64 = 2.0e+00

Then I’m kind of loosing the track how this function was able to get @typeInfo of a pointer at runtime. Probably based on the comptime available information of the pointer type…

Anyway, I think my issue boils down to whether or not I can print out the content of a tagged union at runtime.

You need to switch over the possible tag values of the union at comptime.
This can be done with a switch with inline else. This will automatically create a case statement for each tag value.

fn prettyPrint(node: anytype) void {
    const node_T_info = @typeInfo(@TypeOf(node));
    if (node_T_info == .Union) {
        const optional_tag_type = node_T_info.Union.tag_type;
        if (optional_tag_type) |tag_type| {
            switch(@as(tag_type, node)) {
                inline else => |tag| {
                    std.log.debug("{s}", .{@tagName(tag)});
                    std.log.debug("{any}", .{@field(node, @tagName(tag))});
                }
            }
        }
    }
}
5 Likes

It was my first guess after I revised documentation on tagged unions but it was tough to actually write it. My absolute gratitude! :blush:

2 Likes

By the way, how does this work? I didn’t realize to ask. If it works in runtime, there must be some kind of data the union is carrying with to distinguish which field is active. For example, can I assume that tagged union is like a struct with a tag field that says which field is active and the fields payload itself?:

const TaggedUnion = struct {
    tag: enum { f1_is_active, f2_is_active },
    data: union { f1: u8, f2: u8 },
};

Yeah that’s basically how it works.
And if you know the active tag from some other source and don’t want to pay the runtime cost, then you can just use a regular union.

2 Likes

Also… (hopefully the last follow up)

Even though I’ve read docs with examples on switch several times, which says (best parts):

switch can be used to capture the field values of a Tagged union.

Switch prongs can be marked as inline to generate the prong’s body for each possible value it could have.

The else branch catches everything not already captured.

inline else prongs can be used as a type safe alternative to inline for loops

I still have hard time getting the overall picture of (1) how exactly this switch with inline else and capture work, and (2) whether there is any reason to use @as in switch(@as(tag_type, node)) rather than being explicit as the solution works successfully without. Here what I tried to do:

const std = @import("std");

pub fn main() !void {
    const Node = union(enum) { f1: u8, f2: u8 };
    const node = Node{ .f1 = 255 };
    prettyPrint(node);
}

fn prettyPrint(node: anytype) void {
    const node_T_info = @typeInfo(@TypeOf(node));
    if (node_T_info == .Union) {
        const op_tag_type = node_T_info.Union.tag_type;
        if (op_tag_type) |tag_type| {

            // inlined else
            switch (node) {
                inline else => |tag| {
                    std.log.debug("{any}", .{tag}); // 255
                },
            }

            // inlined else + as
            switch (@as(tag_type, node)) {
                inline else => |tag| {
                    std.log.debug("{any}", .{tag}); // @typeInfo(filename.main.Node).Union.tag_type.?.f1
                },
            }

            // normal else
            switch (node) {
                else => |tag| {
                    std.log.debug("{any}", .{tag}); // filename.main.Node{ .f1 = 255 }
                },
            }

            // normal else + as
            switch (@as(tag_type, node)) {
                else => |tag| {
                    std.log.debug("{any}", .{tag}); // @typeInfo(filename.main.Node).Union.tag_type.?.f1
                },
            }
        }
    }
}

And here is what I get:

debug: 255
debug: @typeInfo(filename.main.Node).Union.tag_type.?.f1
debug: filename.main.Node{ .f1 = 255 }
debug: @typeInfo(filename.main.Node).Union.tag_type.?.f1

As you can see sometimes the capture returns “unwrapped” value of the union field, sometimes it returns a kind of “struct”, sometimes it prints… not sure how to even describe it. After seeing all that, I’m completely lost behind the s-witch magic.

inline else is basically a tool that generates one case for each possible value.
For example these two are functionally equivalent:

var enumValue: enum{a, b} = ...;
switch(enumValue) {
    inline else => |tag| {...}
}
// The above switch statement will basically expand into the following at compile time:
switch(enumValue) {
    .a => {
        const tag = .a;
        ...
    },
    .b => {
        const tag = .b;
        ...
    },
}

And I think your confusion comes from the way tagged unions are treated:

Case statements on tagged unions will automatically unwrap the value(note that this isn’t possible for the normal else, since there are still multiple possible tags at this point which cannot be unwrapped at runtime).

But here we want the tag type, and not the unwrapped union(at least that’s what I assumed from your code). So I used a little trick: Tagged unions can coerce to their tag type, which is an enum. The enum does not get unwrapped and we only get the tag in the inline else.

That’s tough but it’s getting better.

Ok, will then the expansion you gave (in case of a tagged union) look like the following?

var taggedUnionVal: union(enum) { a: u8, b: u8 } = .{ .a = 255 };

// this:
switch (taggedUnionVal) {
    inline else => |val| {
        _ = val;
        // ...
    },
}

// expands to this?
switch (taggedUnionVal) {
    .a => {
        const val = taggedUnionVal.a;
        _ = val;
        // ...
    },
    .b => {
        const val = taggedUnionVal.b;
        _ = val;
        // ...
    },
}

Seems like inline else has completely different meaning than the one without and could be even called differently.

here we want the tag type, and not the unwrapped union(at least that’s what I assumed from your code)

You assumed correctly. The trick with coercing to enum I (finally) got.

What is left unclear is the output of {any}s that yield @typeInfo(filename.main.Node).Union.tag_type.?.f1. Why there is no difference between normal and inline else output? Enum literals cannot be further unwrapped?

Also, I saw this kind of prefix @typeInfo(type).field.field... several times while poking with std.meta but didn’t understood the nature of compiler writing it that way.

Yes, exactly.

I think it still makes somewhat sense in broader context.
Consider the following:

.a => |x| {}, // This is a specific case. Here we can unwrap the tagged union.
.a, .b => |x| {}, // This is a generic case, we cannot unwrap the union, because there is two possible cases at runtime
inline .a, .b => |x| {}, // This generates 2 specific cases at comptime. We can unwrap.
else => |x| {}, // Here we have a generic case of all remaining values. We cannot unwrap.
inline else => |x| {}, // This generates one specific case for each remaining value at comptime. We can unwrap.

Enum literals cannot be unwrapped.(Like what would you expect from unwrapping an enum?) The only difference is that the inline else one is comptime-known, where the other is runtime-known. And since there is no difference in printing these you get the same output.

The "@typeInfo(filename.main.Node).Union.tag_type.?" is just the name that Zig internally gave to this anonymous enum type. In this case it seems like it just chose the origin of the type as its name.
If you construct the tagged union with a named enum type it should give you a better name:

const Enum = enum{ f1, f2 };
const Node = union(Enum) { f1: u8, f2: u8 };
2 Likes

Maybe I misunderstood something but unfortunately, these .a, .b => |x| {} prongs get union unwrapped:

fn change(x: *u8) void {
    x.* = 3;
}

pub fn main() !void {
    var taggedUnionVal: union(enum) {
        a: u8,
        b: u8,
        c: u8,
        d: u8,
        e: u8,
    } = .{ .a = 255 };
    change(&taggedUnionVal.a);
    switch (taggedUnionVal) {
        .a, .b => |x| { // unwrap
            std.log.debug("{any}", .{x});
        },
        inline .c, .d => |x| { // unwrap
            std.log.debug("{any}", .{x});
        },
        else => |x| { // this one indeed keeps x wrapped
            std.log.debug("{any}", .{x}); 
        },
    }
}

Output:

debug: 3

It seems I was wrong here, sorry. The case with multiples values only supports values of similar type. So for example when making a union with a: u8, b: bool it fails.

1 Like

Small typo I think you meant union(Enum) to provide the type from the line above explicitly.

1 Like