Comptime error even though function argument is runtime

I was working on my personal project freightdb, specifically on graph data-structure on a function to update a node, when I run into a error:

src/graph.zig:123:76: error: unable to resolve comptime value
            var updated_node: std.meta.TagPayload(Node, std.meta.activeTag(old_node)) = undefined;
                                                                           ^~~~~~~~
src/graph.zig:123:76: note: argument to function being called at comptime must be comptime-known

This is the function:

pub fn updateNode(self: *Self, tag_str: []const u8, comptime field_name: []const u8, value: anytype) !void {
            const old_node: Node = try self.deleteNode(tag_str);

            var updated_node: std.meta.TagPayload(Node, std.meta.activeTag(old_node)) = undefined;

            inline for (std.meta.fields(@TypeOf(old_node))) |field| {
                @field(updated_node, field.name) = @field(old_node, field.name);
            }
            if (!@hasField(@TypeOf(old_node), field_name)) {
                return error.InvalidField;
            } else if (@TypeOf(value) != @TypeOf(@field(old_node, field_name))) {
                return error.InvalidArgument;
            }

            @field(updated_node, field_name) = value;
            self.nodes.append(updated_node);

            return error.NodeDoesNotExist;
        }

I first thought it was an error on my side but, it just did not go away. I looked into the function throwing the error and it did not have any comptime functions except maybe @TypeOf().

The std.meta.activeTag() function code:

pub fn activeTag(u: anytype) Tag(@TypeOf(u)) {
    const T = @TypeOf(u);
    return @as(Tag(T), u);
}

If additional context is required your can either goto the full project on codeberg or jump directly into the file graph.zig

This whole : std.meta.TagPayload is a type of updated_node variable. Types are evaluated at comptime.

But the error message suggests that old_node is the problem.

The entire std.meta.TagPayload(Node, std.meta.activeTag(old_node)) expression has to be evaluated at comptime. var x: ty is equivalent to var x: comptime ty, there’s no way to have a runtime type.

As a part of that expression, old_node needs to be evaluated at comptime, but it can’t, because it is a runtime value.

1 Like

So there is no solution

I think you have some gaps in your understanding of the semantics of Zig, I don’t know which parts exactly, I think going through https://ziglings.org/ would be a good way to find those with a high likelihood.

Another tip is to write a non generic example of your data structure first, that way you can get all the parts that are independent of generic types right and from there it is then an easier transformation to make that into a generic version. At least I found that helpful when I was starting to write generic things with Zig.

2 Likes

You must decide if you really want this function in compile time or in runtime.
If you want it in compile time, you can try to add as parameters (NewNode: type, OldNode: type) and see if you can fix all the problems.
If you want it in runtime you could create your node metadata at compile time and use them in runtime to do the job.

You are 100% correct, one of the main reason for me making this project is to learn more about Zig and Zig semantics. My thought was if you start hard, it will only get easier from there. Also I had experience writing go code, so that might have affected my judgement. I do not know or understand every bit of Zig but I am improving with each passing day and the roadmap has been quite wonderful. Zig is my favourite language upto this point and I am confident that it will remain in that position.

Interesting idea. I want to keep it at runtime so I might go with the ladder. I say might because I am open to change code and be better

Ignoring the updateNode function, what does @Sze, @matklad and @dimdin think about the rest of my code

Sure you can climb up a cliff-face (I have done that too in the past in different areas), but I think it may cost you additional time, you have to weigh whether that approach has enough additional benefits for you.

Building understanding yourself, instead of memorizing / being taught, is also something I like (combined with reading things and creating little experiments), however in the case of Zig, ziglings was a big part of my learning experience and I think it has saved me some time.
One of the reasons that ziglings is great, is that it has a very short feedback cycle, allowing you to learn principles quickly in a way that is presented clearly.

If you want to go the runtime route, here is a hint for you:
Familiarize yourself with all the ways you can use a switch in Zig.
And ponder about types and what the compiler knows.

I only had a quick look, overall structure looks good.

I think this line could cause you headaches in the future:

Take a look at: Unreachable

I used unreachable so that the function does not return an error.

I think using explicit @panic("OOM") communicates intent better and is better because it always results in a panic, unreachable is for when other invariants make it impossible to happen, but the compiler doesn’t know.

Also if you want to make this into a library, it would become unusable for me with that line in it, I don’t want to use libraries that just decide for me that certain errors aren’t worth handling. If somebody uses it with a fixedbuffer allocator or just is low on memory it could easily happen and they may want their program to react to that instead of just computing a false result (because the node was silently not added).

I also don’t really understand why you wouldn’t just use try and let the function error.
(If you had a function that at comptime pre-computes the upped bound of needed memory and can be used to make sure that enough memory is pre-allocated, that would move the unreachable closer towards “this can’t happen because of invariants”, otherwise it just seems like a case of ‘I don’t want to type @panic("OOM")’ (In that case I would say define a keyboard shortcut))

I think the comptime data parameter of addNode is overly restrictive.
I think one of your main knowledge gaps / misunderstandings is that you seem to think that just because something is runtime, that from that point on nothing about it can be known at comptime, which isn’t true.

If you have tagged union and you switch on it, creating separate switch prongs, then within those prongs it is known at comptime what the type of the value is, because the switch makes sure at run time that that particular branch is only chosen if it has that type.

And because writing all those separate prongs can be annoying if they all contain the same code, Zig has an inline feature for switch.

Language Reference - Inline Switch Prongs

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

With that in mind I would change the addNode function to this:

pub fn addNode(self: *Self, data: T) Tag {
    switch (data) {
        inline else => |val, tag| {
            var node: std.meta.TagPayload(Node, tag) = undefined;
            node.tag = Tag.generate();

            const fields = std.meta.fields(@TypeOf(val));
            inline for (fields) |field| {
                @field(node, field.name) = @field(val, field.name);
            }

            self.nodes.append(@unionInit(Node, @tagName(tag), node)) catch @panic("OOM");
            return node.tag;
        },
    }
}

Additionally I would change the function signature to:
pub fn addNode(self: *Self, data: T) !Tag and use
try self.nodes.append(@unionInit(Node, @tagName(tag), node));

Then in updateNode you could use similar techniques.
I think instead of comptime field_name: []const u8 I would prefer comptime field: <generated enum type that has all possible fields>.

Thanks for the advice @Sze .

1 Like