Clarification on comptime meta-programming

I understand that this is a simple example, but I want to note that it’s not required to make the parameter and the function body comptime. Only the call site has to be comptime. This works as well.

const std = @import("std");

fn decode_array_size(string: []const u8) usize {
    var n: usize = 1;
    for (string) |char| {
        if (char == 'a') n += 1;
    }
    return n;
}

pub fn main() void {
    const n = comptime decode_array_size("aaaaa");
    const array: [n]usize = undefined;
    _ = array;
}

So, in this case decode_array_size can be used both at runtime and comptime.

I think it is harmful to lock some functions behind redundant comptime when they very well may work at runtime too. It introduces additional contraint without any reason (as far as I can see).

3 Likes

It was definitely a weird example. I was intending to imply that this was something we only meant to do at comptime because the constraint is that you cannot set an array’s size at runtime with the value from that function (hence decode_array_size).

So I’ll meet you halfway here - there are functions that can be used meaningfully at runtime, absolutely - don’t wall those off behind a comptime block. If you intend to only make something run at comptime, then comptime blocks are a viable approach :+1:

A better example that is more towards the original point (because again, that was kind of a weird example) would have been something like this within a comptime context:

comptime var n: usize = 1;
var array_1: [n]usize = undefined;
n += 1;
var array_2: [n]usize = undefined;
n += 1;
var array_3: [n]usize = undefined;

Because that actually makes the point I was driving at much better about the need for an allocator at comptime.

2 Likes

This is a great pattern. How might it look if we generalized it to a hierarchy where ThingA calls functions of ThingB and ThingC, and ThingB also uses ThingC ???

Presumably, ThingA could have fields of type ThingB and ThingC; and ThingB could likewise have a field of type ThingC. But don’t these fields really need to be comptime pointers to these structs??? Said another way, there is only one instance of ThingC whose state is manipulated by multiple clients.

The tricky part seems to be in generalizing the allocation of each of these ThingX objects within main(). But maybe it’s not so bad, since in my use-case I “know” the hierarchy is acyclic and hence can have a series of bottom-up comptime var declarations (starting with ThingC); the initializer for ThingB would wire-in a pointer to ThingC, which the initializer for ThingA would have pointers to ThingB and ThingC.

Given this hiearchy, main() simply “calls” on ThingA and the rest simply unfolds naturally. What I still don’t quite see, however, is how to implement one of the original questions at the very beginning of this post by @deckarep :

What I’d like to do is have a function that can process some data using a datastructure, and build some kind of final, static result set at comptime. Then, at runtime my program uses the final result set.

How do I “freeze” the current value of the index field within my ThingX struct??? After a wave of foo() calls at each level of my hiearchy, I could certainly have a freeze() method called when comptime is finished, so to speak. Somehow I’ll need a const whose initializer depends upon the value of index at the time freeze() is called. I’m just not sure how to express this in Zig.

In this instance, you’d freeze it like so:

comptime var thing: Thing = .{}; // make a thing
const frozen = thing; // freeze a thing

I read your example but some pseudo code may help me better understand what issue you are outlining.

Never mind the more complex example, here’s something simpler:

const Thing = struct {
    index: usize = 0,
    fn foo(comptime self: *Thing) void { self.index += 1; }
    fn bar(self: *Thing) void { //use "finalized" value of index }
};

pub fn main() void {
    comptime var thing: Thing = .{};
    comptime thing.foo();
        ...
    const frozen_thing = thing;
    frozen_thing.bar();
        ...
}

In this scenario, it appears that I’ve frozen the “entire” Thing. But what if I just want to freeze the index field as a constant, while having other Thing fields mutable at runtime via bar()???

Said another way, is it possible to implement a Thing.freeze() method which: 1) signals that there will be no more comptime method calls, and 2) that Thing itself should capture mutable fields like index into something that can no longer change.

Besides enforcing the “const-ness” of index at some point in time, one would also hope that the compiler would optimize accordingly.

Somewhat related, let’s assume that I have multiple ThingX types which will have just ONE instantiation in main(); that is to say, they’re singletons. Could I then implement this type using const and var declarations in lieu of fields – not unlike a top-level module container in its own source file???