@struct doesn't respect StructField.Attributes.@"align" values

Given the following code which takes a struct of type T IE {thing1: u32, thing2: u8} generates an SOA (struct of arrays) that are supposed to by aligned to to suggestVectorLength(T); But when actually run, it appears that the alignment values provided to StructField.Attributes.@”align” are erased.

pub fn BoundedMultiArray(comptime T: type, capacity: comptime_int) type {
    const typeinfo = @typeInfo(T);
    const structInfo = switch (typeinfo) {
        .@"struct" => |s| s,
        else => @compileError("T must be of type struct"),
    };

    comptime var names: [structInfo.fields.len + 1][:0]const u8 = undefined;
    comptime var types: [structInfo.fields.len + 1]type = undefined;
    comptime var attributes: [structInfo.fields.len + 1]StructField.Attributes = undefined;
    inline for (structInfo.fields, 0..) |value, i| {
        names[i] = value.name;
        types[i] = [capacity]value.type;
        attributes[i].@"align" = @alignOf(@Vector(suggestVectorLength(value.type) orelse 1, value.type));
        attributes[i].@"comptime" = false;
        attributes[i].default_value_ptr = null;
    }

    names[names.len - 1] = "occupied";
    types[types.len - 1] = [capacity]bool;
    attributes[attributes.len - 1].@"align" = @alignOf(@Vector(suggestVectorLength(bool) orelse 1, bool));
    attributes[attributes.len - 1].@"comptime" = false;
    attributes[attributes.len - 1].default_value_ptr = null;

    const y = @Struct(.auto, null, &names, &types, &attributes);
    return struct {
        data: y,
        pos: usize,

        pub fn init() @This() {
            return .{ .data = undefined, .pos = 0 };
        }

        pub fn testing(this: @This()) !void {
            inline for (names) |value| {
                _ = this;
                //const x = @field(this.data, value);
                //std.debug.print("{any}", .{x});
                std.debug.print("{s}\n", .{value});
            }
        }

        pub inline fn testSearching(this: @This(), comptime field: []const u8, comptime target: anytype) void {
            @setEvalBranchQuota(1_000_000_000);
            const fieldVal = @field(this.data, field);
            const FieldValType = @TypeOf(fieldVal);
            const FieldValTypeInfo = @typeInfo(FieldValType);
            const FieldValArrCast = switch (FieldValTypeInfo) {
                .array => |s| s,
                else => unreachable,
            };

            if (@TypeOf(target) != FieldValArrCast.child) {
                @compileError("Invalid target type");
            }

            const vLenth = suggestVectorLength(FieldValArrCast.child) orelse 1;
            const targetVec: @Vector(vLenth, FieldValArrCast.child) = @splat(target);
            const zeroVec: @Vector(vLenth, u8) = @splat(255);
            const indexVec = genIndexVec(vLenth);

            var finalResult: u64 = std.math.maxInt(usize);
            l: for (0..@divTrunc(capacity, suggestVectorLength(FieldValArrCast.child) orelse 1)) |index| {
                const current: @Vector(vLenth, FieldValArrCast.child) = fieldVal[(index * vLenth)..][0..vLenth].*;
                const result = @reduce(.Min, @select(u8, targetVec == current, indexVec, zeroVec));
                //std.debug.print("{any}", .{index * vLenth});
                //std.debug.print("{any}\n", .{targetVec == current});
                //std.debug.print("{any}\n", .{@select(u8, targetVec == current, indexVec, zeroVec)});
                std.debug.print("{any}\n", .{result});
                if (result != 255) {
                    finalResult = result + (index * vLenth);
                    break :l;
                }
            }

            std.debug.print("Final Result: {any}", .{finalResult});
        }

        pub const InsertErrors = error{CapacityReached};
        pub fn insert(this: *@This(), value: T) !void {
            this.seek() catch |e| {
                switch (e) {
                    error.CapacityReached => {
                        return InsertErrors.CapacityReached;
                    },
                }
            };

            inline for (structInfo.fields) |field| {
                @field(this.data, field.name)[this.pos] = @field(value, field.name);
            }
            @field(this.data, "occupied")[this.pos] = true;
            this.pos += 1;
        }

        pub const SeekErrors = error{CapacityReached};
        pub fn seek(this: *@This()) SeekErrors!void {
            var count: usize = 0;
            const vlength = suggestVectorLength(bool) orelse 1;
            const indexVec = genIndexVec(vlength);
            const nullVec: @Vector(vlength, u8) = @splat(255);
            const occupied: [capacity]bool = @field(this.data, "occupied");
            const target: @Vector(vlength, bool) = @splat(false);

            for (count..@divTrunc(capacity, vlength)) |index| {
                const current: @Vector(vlength, bool) = occupied[(index * vlength)..][0..vlength].*;
                const result = @reduce(.Min, @select(u8, target == current, indexVec, nullVec));

                if (result != 255) {
                    this.pos = result + (index * vlength);
                    return;
                }

                count += vlength;
            }

            return error.CapacityReached;
        }

        fn genIndexVec(vlen: comptime_int) @Vector(vlen, u8) {
            var res: @Vector(vlen, u8) = undefined;
            inline for (0..vlen) |val| {
                res[val] = val;
            }
            return res;
        }

        pub fn getFieldAlignment(this: @This()) void {
            const dataTypeInfo = @typeInfo(@TypeOf(this.data));
            inline for (dataTypeInfo.@"struct".fields) |field| {
                std.debug.print("{s}: alignment attribute = {any}, actual = {any}\n", .{ field.name, field.alignment, @alignOf(field.type) });
            }
        }
    };
}

const std = @import("std");
const suggestVectorLength = std.simd.suggestVectorLength;
const StructField = std.builtin.Type.StructField;

test "can construct" {
    const y = struct { thing1: u8, thing2: u16 };
    var x: BoundedMultiArray(y, 1024) = .init();
    _ = try x.testing();
    const z: u8 = 45;
    try x.insert(.{ .thing1 = 44, .thing2 = 55 });
    try x.insert(.{ .thing1 = 45, .thing2 = 55 });
    x.getFieldAlignment();
    _ = x.testSearching("thing1", z);
}

when running

zig test ./src/utility/BoundedMultiArray.zig
thing1
thing2
occupied
thing1: alignment attribute = 32, actual = 1
thing2: alignment attribute = 32, actual = 2
occupied: alignment attribute = 4, actual = 1
1
Final Result: 1All 1 tests passed.

I’ve taken out any references to other modules so that one could copy and paste this code
0.16.0-dev.2510+bcb5218a2

Am I doing something wrong, or is this a bug?

Consider: align(1) u32 isn’t a type, but u32 is. If I have a field foo: align(1) 32, then @alignOf(fooField.type) = @alignOf(u32) is still going to be 4.

Look at the @offsetOf values for each of the fields to confirm that they’re of the correct alignment.

2 Likes

to clarify and elaborate on what @invlpg said:

The field alignment attribute is the actual alignment, what you thought the actual alignment is just the natural alignment of the type (in other words the default alignment), zig has no way of knowing the type passed to @alignOf is stored in a field of different alignment.

You can test this theory by taking a pointer to the fields, then inspecting the alignment attribute of the pointer (not the alignment of the pointer type)

    const S = struct { a: u32 align(16) };
    const s = S{ .a = 4 };
    const ptr = &s.a;
    // prints `16`
    std.debug.print("{}\n", .{@typeInfo(@TypeOf(ptr)).pointer.alignment});
4 Likes

This is how I understand alignment; I hope my understanding can be helpful rather than cause more confusion XD:

Alignment is a property of ‘location’(lvalue), not a property of ‘type’. Alignment is a property of location independent of the type, rather than something inherent to the type itself.

The parameter of @alignOf is a type, not a lvalue expression. Therefore, @alignOf is not a means to obtain the ‘actual alignment’. @alignOf only gives the ‘default alignment’ of a type. If a location’s alignment is the same as the default alignment of its type, we can omit explicitly setting the alignment for this location, but the alignment of a location is independent of its type.

In C/C++, the default alignment of a type also implies the ‘minimum alignment.’ This means that a location of a given type cannot have an alignment smaller than the default alignment for that type. Therefore, we can assume that the specified alignment of a struct field reflects its actual alignment.

In Zig, the alignment of a location can be lower than its default alignment. Therefore, for struct fields, if the struct location itself is given a smaller alignment, the alignment of the struct fields will also be downgraded, meaning the alignment of the struct fields is not the ‘actual’ alignment.

This can be seen using a pointer type to the field:

const T = struct {
    a: u64 align(16),
};
test "alignment" {
    const t: T align(1) = .{ .a = 1 };
    const ptr_to_a = &t.a;
    std.debug.print(
        "\n{d}\t{s}\n",
        .{ @typeInfo(T).@"struct".fields[0].alignment, @typeName(@TypeOf(ptr_to_a)) },
    );
}
[2026-02-11T04:57:58.386Z] Running test: main.zig - alignment
1/1 main.test.alignment...
16      *align(1) const u64
OK
All 1 tests passed.
1 Like

Alignment is a property of location, not the value. You interact with values via location so you won’t really notice the difference.

But it can be observed with a simple example

const s: struct { a: u32 align(16) } = .{ .a = 5 };
const i: u32 align(1) = s.a;
// the value goes from a 16 aligned location to a 1 aligned location

I should have mentioned this!!

2 Likes

Thank you for your correction; your way of saying it is better. I should indeed have conveyed the meaning of ‘location,’ but I failed to find the most accurate word to express it. I apologize because English is not my native language.

1 Like