How to change var in comptime block?

I need a global variable that contains a simple (statically known size) tree of types. Here is how I originally did it:

const Type = enum {
    Type,
    Number,
    Int,
    Int32,
    Int64,
    Float,
    // ...
};

const Node = struct {
    self: Type,
    next: []const Node,
};

var tree: Node = Node{ .self = Type.Type, .next = &[_]Node{
    Node{ .self = Type.Int, .next = &[_]Node{
        Node{ .self = Type.Int32, .next = undefined },
        Node{ .self = Type.Int64, .next = undefined },
    } },
    Node{ .self = Type.Float, .next = undefined },
} };

pub fn main() !void {
    std.log.debug("{any}", .{tree});
}

Program gives the expected result:

debug: test.Node{ .self = test.Type.Type, .next = { test.Node{ .self = test.Type.Int, .next = { ... } }, test.Node{ .self = test.Type.Float, .next = { ... } } } }

Later on, I didn’t like the fact that I have to put all these struct initializers at the same time I define the tree. So I tried to do all the dirty stuff within the labeled block:

var tree: Node = blk: {
    var root = Node{ .self = Type.Type, .next = &[0]Node{} };

    // Integers
    var int = Node{ .self = Type.Int, .next = undefined };
    const int32 = Node{ .self = Type.Int32, .next = undefined };
    const int64 = Node{ .self = Type.Int64, .next = undefined };
    int.next = &[_]Node{ int32, int64 };
    root.next = root.next ++ [1]Node{int};

    // Floats
    const float = Node{ .self = Type.Float, .next = undefined };
    root.next = root.next ++ [1]Node{float};
    // ...

    // Other types
    // ...

    break :blk root;
};

Thankfully, it just worked as expected. Next, out of curiosity, I recalled that there is a comptime {} block, which should do the same “trick” as the labeled block did:

var tree: Node = undefined;
comptime {
    var root = Node{ .self = Type.Type, .next = &[0]Node{} };

    // Integers
    var int = Node{ .self = Type.Int, .next = undefined };
    const int32 = Node{ .self = Type.Int32, .next = undefined };
    const int64 = Node{ .self = Type.Int64, .next = undefined };
    int.next = &[_]Node{ int32, int64 };
    root.next = root.next ++ [1]Node{int};

    // Floats
    const float = Node{ .self = Type.Float, .next = undefined };
    root.next = root.next ++ [1]Node{float};
    // ...

    // Other types
    // ...

    tree = root;
}

However, this time the program did not compile:

src/test.zig:48:10: error: unable to evaluate comptime expression
    tree = root;
    ~~~~~^~~~~~
src/test.zig:48:5: note: operation is runtime due to this operand
    tree = root;
    ^~~~

What’s the matter? It feels like I should be able somehow to init this tree var during the compilation.

I remember people were trying to do something similar:

var memory: [128]u8 = undefined;
comptime {
    // writing directly
    _ = try std.fmt.bufPrintZ(&memory, "{s}", .{"Hello world!"});

    // or using an allocator interface
    var fba = std.heap.FixedBufferAllocator.init(&memory);
    const alloc = fba.allocator();
    _ = try std.fmt.allocPrintZ(alloc, "{s}", .{"Hello world!"});

    // the former does the same thing as the latter anyway..
}

But this doesn’t compile either.

comptime {} and label: {} blocks aren’t interchangeable since labelled blocks are not enforcing compile-time execution. At compile-time you can neither heap-allocate or use pointers to runtime variables.

You can’t change runtime variables from withing a comptime block.

Luckily in your case there is a simple solution. You can just use a labelled comptime block:

var tree: Node = comptime blk: {
    ...
    break :blk root;
};
3 Likes

labelled blocks are not enforcing compile-time execution

That is strange. Why then the compiler complains about using comptime in front of it (as @IntegratedQuantum suggested):

var tree: Node = comptime blk: {
    ...

Gives:

src/test.zig:29:18: error: redundant comptime keyword in already comptime scope

Could you give an example please?

Update. Never mind. I thought you were stating affirmatively. I missed the negating neither part.

That’s because global variable initialization is always done at compile time. So your old block already was executed at comptime.

3 Likes

Your first version looks more readable to me you also could drop a bunch of things that can be inferred from context:

const Node = struct {
    self: Type,
    next: []const Node = &.{}, // default value to avoid having to specify it
};

var tree = Node{ .self = .Type, .next = &.{
    .{ .self = .Int, .next = &.{
        .{ .self = .Int32 },
        .{ .self = .Int64 },
    } },
    .{ .self = .Float },
} };

I don’t like the names self and next, so I changed them, but this is subjective and may depend on more context.
You also could use helper functions to make it even shorter:

const Node2 = struct {
    type: Type,
    children: []const Node2 = &.{},
};

fn node(t: Type, c: []const Node2) Node2 {
    return .{ .type = t, .children = c };
}
fn leaf(t: Type) Node2 {
    return .{ .type = t };
}

var other_tree = node(.Type, &.{
    node(.Int, &.{
        leaf(.Int32),
        leaf(.Int64),
    }),
    leaf(.Float),
});
3 Likes