How do you generate struct fields at comptime with struct functions?

Hi all, I am trying to generate fields on a struct at comptime. Ideally, I’d like to be able to do something like this to generate a Vec3.

const Vec3f32 = Vec({ "x", "y", "z" }, f32);

I was able to generate something like this using a strange syntax I found here in the Zig standard library.

pub fn Vec(comptime T: type) type {
    const fields = [_]std.builtin.Type.StructField{
        .{ .name = "x", .type = T, .default_value = null, .is_comptime = false, .alignment = 0 },
        .{ .name = "y", .type = T, .default_value = null, .is_comptime = false, .alignment = 0 },
        .{ .name = "z", .type = T, .default_value = null, .is_comptime = false, .alignment = 0 },
        .{ .name = "w", .type = T, .default_value = null, .is_comptime = false, .alignment = 0 },
    };
    return @Type(.{ .Struct = .{
        .layout = .auto,
        .fields = fields[0..],
        .decls = &.{},
        .is_tuple = false,
    }});
}

However, I’m not sure how to add functions to the struct using this syntax. How would I write a Vec function like this that would generate full structs like the one below, given a set of field names and a number type?

const Vec3f32 = struct {
    x: f32,
    y: f32,
    z: f32,

    pub fn add(self: Vec3f32, other: Vec3f32) Vec3f32 {
        return Vec3f32{ .x = self.x + other.x, .y = self.y + other.y, .z = self.z + other.z };
    }

    pub fn dot(self: Vec3f32, other: Vec3f32) f32 {
        return self.x * other.x + self.y * other.y + self.z * other.z;
    }
};
// More Vec functions...

Thanks in advance!

Hi @christianphalv, welcome to Ziggit! This is intentionally not possible: Allow declarations in @Type for Struct/Union/Enum/Opaque · Issue #6709 · ziglang/zig · GitHub There is no way to add declarations (including functions) to types created using @Type.

However, you could potentially work around this by moving the functions out of the generated types and making them generic, e.g.

pub fn add(comptime Vec: type, a: Vec, b: Vec) Vec {
    var result: Vec = undefined;
    inline for (@typeInfo(Vec).@"struct".fields) |field| {
        @field(result, field.name) = @field(a, field.name) + @field(b, field.name);
    }
    return result;
}

test add {
    const a: Vec3f32 = .{ .x = 1, .y = 2, .z = 3 };
    const b: Vec3f32 = .{ .x = 1, .y = 2, .z = 4 };
    const res = add(Vec3f32, a, b);
    try std.testing.expectEqual(2, res.x);
    try std.testing.expectEqual(4, res.y);
    try std.testing.expectEqual(7, res.z);
}

The main downside of this approach is that it no longer allows you to use method call syntax like a.add(b).

2 Likes

Thanks for the detailed response! It’s unfortunate that this isn’t possible in the language. It’s seems weird that you can 1) generate fields at comptime and 2) access fields at comptime in a function, but not put those two ideas together in the same struct.

If your goal is to have x, y, z fields with shared methods for different Ts, this might be an alternative approach. The original idea there was to use extern structs to make bit casting to arrays and @Vectors possible. The current gist doesn’t use usingnamespace, but an older revision does if you prefer since thats simpler.

If I were doing this now, I might not use extern structs nor use simd() to implement add(). Instead I would inline loop over the fields and let the compiler auto vectorize since simd can often produce worse code that auto vectorization.

EDIT: I don’t like that you have to access the shared methods via vec.m.add(...) with that approach though (the m field is needed for mixins and is zero sized). I actually prefer the old usingnamespace way. Or this approach where shared methods need to be synced but only in a few places and checked at comptime with check(VecN(AnyT)) once for each N.

2 Likes