Why can't values of a struct containing function fields be run-time mutable?

No a field that uses comptime field: T = val is different from one that doesn’t field: T no matter what type that field has.

If the type of the field can only be used at comptime, like type, comptime_int, comptime_float or function body types, then instances of that struct type can only be used at comptime or in constants. And thats it.

You can’t mutate things at runtime that contain comptime-only values within fields.
The comptime field needs to be seen as not a normal field it essentially gets compiled out as a constant and isn’t part of any runtime value.

This is why I say comptime is not a good annotation name for struct fields. The values of the such fields are confirmed at the beginning (instead of during the process) of a compilation.

I don’t know what you mean with ‘confirmed at the beginning’, I don’t think that is true. Take a look at this:

const std = @import("std");

fn ConstValue(comptime value: anytype) type {
    return struct {
        comptime value: @TypeOf(value) = value,
    };
}

pub fn main() !void {
    comptime var calc = 4;
    calc += 25;
    calc *= calc;
    calc -= 3;

    const val: ConstValue(calc) = .{};
    std.debug.print("val: {}\n", .{val.value});
}

// val: 838

That is just one way, you also could create a struct using comptime code via @Type, so it is very much possible to create different struct types that have comptime fields with values that are based on other comptime code.

Comptime code execution happens during compilation / interleaved with code generation.

1 Like

comptime fields are pretty esoteric and as far as I know there are only two contexts in which they are useful.


The first and very situational one is if there is a function that takes an anytype parameter and expects the passed struct instance to have a certain field like size (i.e. the function accesses instance.size) and you have a struct that implements that duck-typed anytype interface, but the value for that field is constant and comptime-known and you want to implement the interface without the field taking up memory at runtime. If size: u64 is runtime-known, that’s another 8 bytes added to the total size of the struct. comptime size: u64 = 1024 on the other hand does not affect the size at all.

(As a parallel, both []T and *[2]T expose the synthesized fields ptr and len and are both treated as “slice-like” by the language. []T is 16 bytes in size (on 64-bit targets), but *[2]T is only 8 because the value for its len field is comptime-known.)

But comptime fields as a language feature is not strictly necessary to save space, you could just as well redesign the function/interface to obtain the size via a method like instance.getSize() instead.


The second and much more useful one comes from how anonymous tuples/structs behave. If you have an expression like const x = .{foo}, x will either be struct { @"0": T } or struct { comptime @"0": T = ??? } depending on whether foo is comptime-known or not. This is very useful because it means that you can detect the comptime-knownness of a value with just @typeInfo(@TypeOf(.{foo})).Struct.fields[0].is_comptime and use it for things like taking different optimized paths that only work if some value is comptime-known.

Anonymous tuples capturing comptime-knownness is also important for @call() when calling inline functions (as a reminder, inline functions propagate comptime-knownness). If they didn’t capture this information, @call(.auto, someInlineFn, .{ arg0, arg1 }) would not be able to employ the full power of inline functions since it would result in all arguments being treated as runtime-known even if their values are technically statically known.

I know some people on the compiler team don’t like comptime fields (because they are weird and very easy to misunderstand) and want to remove them, but @call()ing inline functions is an important and useful enough use case that they can’t remove the feature without coming up with a suitable replacement.

4 Likes

Sorry, some my zig understanding is still in chaos. My old description is not really not accurate, or even correct. What I exactly mean is there are two semantics of compile-time fields, both of them are not what I expected.

My use case is to to let a user config, which has many options, contain some run-time options and some compile-time options. It looks this is impossible now. But if I divide the config into many function parameters, some of them are comptime, then it is possible.

1 Like