Can tuple fields be mutated?

I can’t say I’ve used tuples a lot, and my question is quite newbish: can tuple fields be mutated at all?

For example, this code doesn’t compile:

  var a = .{ 1, 2, 3};
  a[0] = 10;
tuples.zig:8:10: error: value stored in comptime field does not match the default value of the field
    a[0] = 10;
    ~~~~~^~~~

std.debug.print("{s}\n", .{ @typeName(@TypeOf(a)) }); gives me this (after adding _ = &a; to make the compiler happy):

struct{comptime comptime_int = 1, comptime comptime_int = 2, comptime comptime_int = 3}

Fields are comptime_int, and so I can’t modify their values.

Alright, let’s change a’s initialization to var a = .{ @as(u32, 1), @as(u32, 2), @as(u32, 3) };
Same error:

tuples.zig:8:10: error: value stored in comptime field does not match the default value of the field
    a[0] = 10;
    ~~~~~^~~~

Type of a is now struct{comptime u32 = 1, comptime u32 = 2, comptime u32 = 3}.

The reason I’m asking this question is not because I want to mutate tuple fields, but because I’m trying to write a comptime struct populating function, and since tuples are structs, I want to know what should this function do if it gets a tuple, or an anonymous struct like var a = .{ .x = @as(u32, 1), .y = @as(u32, 2), .z = @as(u32, 3) }; where fields are also comptime: struct{comptime x: u32 = 1, comptime y: u32 = 2, comptime z: u32 = 3}

Using @typeInfo(field.type) on tuple’s fields doesn’t tell me if the field is comptime or not, all I get is that it’s an unsigned 32-bit integer: builtin.Type{ .Int = builtin.Type.Int{ .signedness = builtin.Signedness.unsigned, .bits = 32 } }

Comptime fields are like Model-Ts. You can set them to any value, as long as it’s their default value:

const std = @import("std");

const S = struct {
    number1: u32,
    number2: u32,
    comptime diff: u32 = 1,
};

pub fn main() void {
    comptime var s: S = undefined;
    s.number1 = 4;
    s.number2 = 5;
    s.diff = s.number2 - s.number1;
    std.debug.print("{any}\n", .{s});
    s.number1 = 7;
    s.number2 = 8;
    s.diff = s.number2 - s.number1;
    std.debug.print("{any}\n", .{s});
}
test.S{ .number1 = 4, .number2 = 5, .diff = 1 }
test.S{ .number1 = 7, .number2 = 8, .diff = 1 }
1 Like

So, fields of tuples and anonymous structs are basically const (well, almost).
This means I have to be able to filter them out somehow.

You wouldn’t happen to know how this is possible? As I have mentioned, @typeInfo(field.type) is not of much help, it doesn’t tell me if the field is comptime or not. For example, all the information it gives on integer fields is this: zig/lib/std/builtin.zig at d03649ec2f42c6b967f7113e78aa137de5384c1a · ziglang/zig · GitHub

Strangely, @typeInfo(field.type) returns Type.Int, not Type.ComptimeInt.

Here’s my code (minus comments):

const std = @import("std");

pub fn main() void
{
    var a = .{ @as(u32, 1), @as(u32, 2), @as(u32, 3) };
    var b = .{ .x = @as(u32, 1), .y = @as(u32, 2), .z = @as(u32, 3) };
    _ = &a; // makes compiler happy. `&` promotes a to l-value
    _ = &b; // makes compiler happy. `&` promotes b to l-value
    printInfoAboutStruct(@TypeOf(a), 0);
    printInfoAboutStruct(@TypeOf(b), 0);
}

fn printInfoAboutStruct(comptime T: type, comptime recurs_lvl: u32) void
{
    const info = @typeInfo(T);
    std.debug.print("{s}[{s}]\n", .{ "    " ** recurs_lvl, @typeName(T) });
    inline for (info.Struct.fields, 1..) |field, i|
    {
        switch (@typeInfo(field.type))
        {
            .Struct => //|struct_info|
            {
                std.debug.print("{s}{d}. `{s}: {s}` (which is a struct)\n",
                                .{ "    " ** (recurs_lvl + 1), i, field.name, @typeName(field.type) }, );
                printInfoAboutStruct(field.type, recurs_lvl + 1);
            },
            else =>
            {
                std.debug.print("{s}{d}. `{s}: {s}`\n{s}--> {any}\n",
                                .{ "    " ** (recurs_lvl + 1), i, field.name, @typeName(field.type),
                                   "    " ** (recurs_lvl + 2), @typeInfo(field.type) } );
            }
        }
    }
}

The output is

[struct{comptime u32 = 1, comptime u32 = 2, comptime u32 = 3}]
    1. `0: u32`
        --> builtin.Type{ .Int = builtin.Type.Int{ .signedness = builtin.Signedness.unsigned, .bits = 32 } }
    2. `1: u32`
        --> builtin.Type{ .Int = builtin.Type.Int{ .signedness = builtin.Signedness.unsigned, .bits = 32 } }
    3. `2: u32`
        --> builtin.Type{ .Int = builtin.Type.Int{ .signedness = builtin.Signedness.unsigned, .bits = 32 } }
[struct{comptime x: u32 = 1, comptime y: u32 = 2, comptime z: u32 = 3}]
    1. `x: u32`
        --> builtin.Type{ .Int = builtin.Type.Int{ .signedness = builtin.Signedness.unsigned, .bits = 32 } }
    2. `y: u32`
        --> builtin.Type{ .Int = builtin.Type.Int{ .signedness = builtin.Signedness.unsigned, .bits = 32 } }
    3. `z: u32`
        --> builtin.Type{ .Int = builtin.Type.Int{ .signedness = builtin.Signedness.unsigned, .bits = 32 } }

Yeah, it’s a constant masquerading as a struct field. field.is_comptime indicates whether a field is fake or not. The terminology is a bit confusing. Comptime fields aren’t the same thing as fields with comptime-only types.

4 Likes

Non-comptime tuple fields can be mutated:

var a: struct { u32, u32, u32 } = .{ 1, 2, 3 };
a[0] = 10;
std.debug.print("{any}\n", .{a});
// -> { 10, 2, 3 }

But as you’ve already identified, a tuple literal like
.{ 1, @as(u32, 2), @as([]const u8, "3") }
gets the type
struct { comptime comptime_int = 1, comptime u32 = 2, comptime []const u8 = "3" }
unless you explicitly coerce it to a different compatible type (with runtime fields).

2 Likes

Oh, it’s as simple as that.
Thank you very much!

Thank you for mentioning that tuples can have non-comptime fields as well.

Using .is_comptime should help me differentiate between comptime and non-comptime fields.

A curious semantic quirk with comptime fields is how the keyword makes a struct with fields with comptime types available at runtime:

const std = @import("std");

const S1 = struct {
    comptime number: comptime_int = 1,
    comptime literal: @TypeOf(.enum_literal) = .hello,
    state: bool = false,
};

const S2 = struct {
    number: comptime_int = 1,
    literal: @TypeOf(.enum_literal) = .hello,
    state: bool = false,
};

pub fn main() void {
    var s: S1 = .{};
    var k: i32 = 0;
    std.debug.print("{any}\n", .{s});
    std.debug.print("{x}\n", .{@intFromPtr(&s)});
    std.debug.print("{x}\n", .{@intFromPtr(&k)});
}

Change the type of s to S2 and Zig will demand that you change it to const.

2 Likes

Yes, I have noticed that I was able to add comptime modifier to otherwise “normal” struct fields. I wonder if such fields are associated with struct instances, like normal fields are, or with the struct type, like consts that are declared within struct types. I guess the former is true.

I’m scratching my head here. S2 has default values for all its fields… but number is still comptime_int :face_with_raised_eyebrow:

Can you even declare a variable that is NOT comptime (or a struct field that is not comptime) of a comptime_* type?!

Comptime fields are functionally exactly the same as a const declared on the struct. As I said, it’s just a constant masquerading as a struct field. It’s useful in situation where you want to handle the constant and variable part of a struct in the same manner. Suppose you have a file header:

const HeaderA = struct {
    comptime signature: u32 = 0x4F92_2AED,
    comptime version: u16 = 0x11,
    size: u64,
    attributes: Attributes,
    comptime reserved1: u32 = 0,
    comptime reserved2: u32 = 0,
    crc32: u32,
};

Because the signature, version, and the reserved fields don’t change, there is no reason to allocate space for them. By pretending that they’re struct fields, we are still able to write them to file using a simple inline loop:

inline for (@typeInfo(@TypeOf(header).Struct.fields) |field| {
    file.write(@field(header, field.name));
}

Functionally the struct is the same the following:

const HeaderA = struct {
    const signature: u32 = 0x4F92_2AED;
    const version: u32 = 0x11;
    const reserved1: u32 = 0;
    const reserved2: u32 = 0,

    size: u64,
    attributes: Attributes,
    crc32: u32,
};

To write out this struct, we’d need to hand-code each field, since we’ve lost the information concerning the position of the fields relative to each other (specifically that reserved1 and reserved2 lie between attributes and crc32).

5 Likes

To answer the second part of your post, S2 is a comptime-only struct due to the presence of two members with comptime types (comptime_int and @TypeOf(.enum_type), a type so rarely used that it didn’t get a keyword). The use of “comptime” in S1 constricts the comptime-ness to just the fields in question, stopping them from contaminating the rest of the struct. That’s why the struct can be created at runtime.

3 Likes

I think I understood it… but I’n not certain.
This is truly esoteric stuff, especially that @TypeOf(.enum_literal).