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

For example, the following code doesn’t compile.

const Foo = struct {
    x: bool,
    comptime y: u8 = 123,
    f: fn() void = bar, // compiles okay if this line is removed
};

fn bar() void {}

pub fn main() !void {
    var foo = Foo{.x = true};
    foo.x = false;
}

You need to use a function pointer. Documentation - The Zig Programming Language

See the example code in the language reference for functions. There is no dedicated section for function pointers.

4 Likes

I’m aware of this. I just don’t get the reason now.

the type of f above is not a function pointer type but only a bare function type. You need to place a * before the type. Note that because fonction pointers point to constant data, you also need to place a const.

Here is an example from one of my projects:

pub const Pointer = *const fn (?*anyopaque, []VmValue) anyerror!VmValue;

I believe the reasoning here is the same as for other pointers: the pointee type is separated from the pointer type.

Note that in hardware, function pointers are the same as other pointers: these are adresses of something inside the computer’s RAM

1 Like

Ah, if the function field is annotated with comptime, then it compiles.

const Foo = struct {
    x: bool,
    comptime y: u8 = 123,
    comptime f: fn() void = bar, // compiles okay now
};

fn bar() void {}

pub fn main() !void {
    var foo = Foo{.x = true};
    foo.x = false;
}

I thought function fields are comptime implicitly before. So it looks this understanding is wrong.

I still haven’t got the reason why the code in the first comment fails to compile.

You already got your answer:
Function-body types can only be used at comptime.

But your original example tried to use them at run-time.

Function-body types are essentially only an identity that represents some function at compile time, once these functions get compiled into a program you can convert that identity to the corresponding pointer to the code of that function (wherever the compiler decides to put it) with the & address operator.

It is a struct field, function fields don’t exist.

1 Like

I mean “fields of function types”.

The original example doesn’t use the field of function type at run-time.
But maybe you are right, it can be potentially used at run-time, at least from the semantic level. That is why the field needs an explicit comptime annotation.

Yes, you should be able to use the original Foo struct in a comptime var, but aren’t allowed to use it in a normal var, because then you could change the .f field at runtime which is impossible because values of its type don’t exist at runtime.

1 Like

Technically in hardware, pointers don’t exist. You just tell the hardware to use some number as an address and that’s it.

Well, with the exception of CHERI hardware of course.

2 Likes

A new question: it looks a comptime field must be specified an initial value and its value can’t be values other than the specified initial value.

Then what is the meaningfulness of the comptime fields? They are equivalent to const variable declarations, right?

You can access them via instance.field_name, with const declarations you can only use Type.const_declaration, I think the former can be potentially useful in some situations where some code uses comptime ducktyping and always expects a specific field to be there and then you have a type where that field needs to be there, if it can’t ever be anything different. In that case using a comptime field allows you to keep the same ducktyping interface without needing to have runtime mutable data for the field.

That said, I am not sure whether this gets frequently used anywhere.

Regarding:

you also could do this:

const std = @import("std");

const Foo = struct {
    x: bool,
    y: u8 = 123,
    f: fn () void = bar,
};

fn bar() void {}
fn baz() void {
    std.debug.print("baz", .{});
}

pub fn main() !void {
    comptime var foo = Foo{ .x = true };
    foo.x = false;
    foo.f = baz;

    foo.f();
}

You can create Foo instances if those are used/mutated at comptime, if you also want to change them at runtime you need to use function pointers instead, so that the Foo-instance can exist at runtime.

1 Like

Thanks for the detailed explanations. Learned much.

I just still don’t get why can’t

const Foo = struct {
    x: bool,
    f: fn() void = bar,
};

be partial compile-time known and partial run-time known.

And I also don’t get why comptime fields are immutable at compile time.

Because = in a struct field does not set a value at compile time.
It sets the default value when the structure is constructed and this happens at runtime.

e.g.

y: u8 = 123,

Does not mean set 123 to y. y can get any u8 value. 123 is the initial value if y is not specified in the struct construction.

2 Likes

I have no confusion about non-compile-time general fields. I just have confusions on compile-time fields and non-compile-time function fields.

For example, I don’t understand the reasons for the following two errors.

const Foo = struct {
    comptime y: u8 = 123,
};

const Bar = struct {
    z: u8 = 123,
    f: fn() void = dummy,
};

const Wee = struct {
    w: u8 = 123,
    comptime f: fn() void = dummy,
};

fn dummy() void {}

pub fn main() !void {
    comptime var x: u8 = 123;
    x = 99; // okay
    if (x != 99) @compileError("bad");
    
    comptime var foo: Foo = .{};
    foo.y = 99; // error
    
    var wee: Wee = .{};
    wee.w = 99; // okay
    
    var bar: Bar = .{};
    bar.z = 99; // error
}

I think that would be awkward for a compiled language that evaluates most code eagerly, because it would add a ton of possible combinations for how the code could be run / compiled, when specific parameters are comptime known in one place, but runtime known in another you would end up with different concrete types that aren’t compatible.

Also you would completely lose the clear separation between comptime var and var. I just don’t think this fits the language, it would be to implicit and messy (if you try to avoid fully redesigning the language towards allowing partial comptime evaluation everywhere, which probably would make it a very different language).

I think having partial evaluation as a general concept in a compiled language is a thing you would start building your language with, not something that you tack on later.

So “why can’t I just” seems like too much magical thinking here, you can’t just add a feature, it needs to fit and I don’t know how you would fit this into Zig, without a lot of other things breaking.

But because this is explain and not brainstorming, we also can just say:
no there are no partially comptime evaluated struct values, you either evaluate at comptime or runtime, not half comptime and half runtime

Instead you can put the comptime parts in a struct that is used at comptime
and then when you are done doing comptime things copy the values into a runtime struct that may have more other fields beyond those comptime ones.

The benefit of that is that it makes it clear what happens at comptime and what at runtime.

1 Like

Maybe the confusion comes from when a field is annotated with comptime. For explicitly annotated comptime fields, comptime should be read as readonly.

Type and function fields are really compile-time fields, if they are not annotated with comptime.

Because you want to change it at runtime. A comptime/const field can’t be changed at runtime, by definition.

The answer is simple comptime fields are constant and have the value they are declared with, that is simply how it is.
You can ask why, but the answer is, because Zig implemented it that way.

I think the compiler gives the best explanation:

comptimefields.zig:28:14: error: variable of type 'comptimefields.Bar' must be const or comptime
    var bar: Bar = .{};
             ^~~
comptimefields.zig:7:8: note: struct requires comptime because of this field
    f: fn () void = dummy,
       ^~~~~~~~~~
comptimefields.zig:7:8: note: use '*const fn () void' for a function pointer type

What I want to change are the non-comptime and non-const fields of a struct which contains non-comptime function/type fields, not the function/type fields.

I know the current rules now. It just seems that annotating fields with comptime is not a good idea.