Persistent comptime variables -- config params

using zig, i want to emulate a programming construct called a config – which is to say, a comptime var that becomes a const at runtime… furthermore, a config is NOT local to a function but rather a top-level (pub) feature of a struct or compilation unit…

the idea, of course, is that a config is manipulated at comptime – just like any other persistant state… but once comptime is “finished”, the last value bound to the config becomes a const further downstream…

i know that top-level variables can be initialized with comptime values resulting from arbitrary comptime calculations… but once finished, i want this variable to effectively “freeze”…

thoughts on a zig design pattern that emulates this behavior ???

Assuming the config depends on user input, you can make use of this build system pattern.

let me try my best to create an example, hopefully using zig-isms correctly as i come up the learning curve…

i want to create a module modA which exposes a “property” named param which is mutable at comptime but constant at runtime…

following common practice, i could expose param via a pair of pub getter/setter functions – which internally read/write some object…

the semantics i’m trying to emulate effectively say that my setParam function can ONLY execute at comptime, whereas my getParam function can execute in any context…

assuming i represent param as a “private” variable within modA (accessible only via my getter/setter), will the backend compiler (LLVM) notice that param (initialized with a value comptime) is NEVER written at runtime??? and if so, would LLVM effectively demote this variable to a known constant and optimize according???

to rephrase the question, can i ensure that a function is ONLY called in a comptime context??? obviously i could include some code in its body that could ONLY be called at comptime… or is there some sort of built-in zig function that i could use to query where i am (so to speak) in the compilation process???

Sure - we’ve talked about how to guarantee this is in other threads (you may want to try searching for that), but you can always do:

const x = comptime foo();

Where foo is an arbitrary function.

2 Likes

yes, that works… but it assumes the caller has added the comptime at the call site…

correct me if i’m wrong, but couldn’t i simply wrap the body of foo with a comptime { ... } block???

this would not only assert that the implementation of foo is comptime compatible, but would also relieve the caller from having to explictly say comptime… if they are already in a comptime context, the call would be fine; and if they are truly in a runtime context, the compiler would issue an error at the call site – which is just what i want…

quite simply, the documentation for foo would say that it MUST be called at comptime only…

You can make it an error if it’s called at any other time, sure - there’s a builtin for that: Documentation - The Zig Programming Language

I think the most important thing here is knowing when Zig is automatically in comptime. Function calls that happen on global/file-level variables get initialized at comptime, but anything that’s run from main is runtime. In the second case, you need to specify comptime.

1 Like

Here’s an example:

pub fn foo() i32 {
    return 42;
}

var x = comptime foo();

pub fn main() !void {
 // your runtime code...
}

You’ll get the following error here:

example.zig:5:9: error: redundant comptime keyword in already comptime scope

Since it’s global, foo is already invoked at comptime.

Yes, you can force any function to be comptime only, by marking all the parameters as comptime and add comptime after return.

fn add(comptime x: i32, comptime y: i64) i32 {
    return comptime x+y;
}

const foo = add(41,1);

this is equivalent to const foo: i32 = 42;

Normally, nobody does that, because without adding any comptime keywords you get the same result.

2 Likes

Similar to what @dimdin just posted, you can wrap it too:

pub fn foo() i32 {
    return 42;
}

pub fn bar() i32 {
    return comptime foo();
}

export fn baz() i32 {
   return bar();
}

As was mentioned though, if you add parameters, those need to be deducible at comptime time. You can’t give your function runtime only data and try to run it at comptime. I hope this helps :slight_smile:

1 Like

so here’s what i just discovered in my sandbox…

const expect = @import("std").testing.expect;

const Ctr = struct {
    val: u32 = 0,
    pub fn create(comptime val: u32) Ctr {
        comptime {
            return Ctr{ .val = val };
        }
    }
    pub fn next(self: *Ctr) u32 {
        self.val += 1;
        return self.val;
    }
};

test "ctr test" {
    var ctr: Ctr = comptime Ctr.create(10);
    try expect(ctr.next() == 11);
}

pub fn main() !void {}

if i do NOT add comptime in my test, the compiler gives me an error – which is what i want… i could also wrap the entire body of my test in a comptime { }, since my next function can be called in this context…

all of the comments above have been VERY helpful to me… i’m trying to impose certain patterns on my use of zig… anything that can be done in the simple example above to further streamline the code – and to communicate intent is greatly appreciated… :pray:

i’ll shortly publish a gist for the original question about implementing what i termed a config param…

2 Likes

This thread triggered a memory deep in my brain basement. lol In this excellent Zig Showtime episode featuring Mitchell Hashimoto talking about his Ghostty terminal project, at about 43:51 he shows how if he wants to guarantee that a function is only evaluated at comptime, he wraps the body in a comptime block. So yeah, wrapping the body is OK. :^) https://youtu.be/l_qY2p0OH9A?si=wWSunjbr_JxjZwh3&t=2631

2 Likes