Comptime type array generation with new memory restrictions

In my GA library zilliam, more specifically with the Blades system, I generate an array of types each representing a mask for a big algebra. This is to use less resources and compute less, and their member functions have special logic that allows their result value to vary based on the input types, and those return types are all contained within the type array. This is a (very) simplified version of what I describe:

pub const Collection: type = blk: {
    var TypeArray: [3]type = undefined;

    for (&TypeArray, 0..) |*T, i| {
        T.* = struct {
            arg: usize,
            pub const Index = i;
            pub fn func() TypeArray[3 - i] {
                return .{ .arg = TypeArray[3 - i] };
            }
        };
    }

    const final = TypeArray;
    break :blk struct {
        pub const Types = final;
    };
};

comptime {
    @compileLog(Collection.Types[0].func());
}

But with the new comptime var restrictions, this errors out, as it access a pointer (the TypeArray) to comptime variable memory. One solution I thought was to implement the operators (or in the simplified case, func) outside the struct definition, and in another type, so something like CollectionFunctions.func(Collection.Types[0]) in this case. Then those types won’t actually contain any comptime var pointers. I’m wondering whether there’s any other ideas from people here? My solution is fine, but will require a lot of refactoring due to changing how all operators work.

1 Like

I’m a little confused at what this code is trying to do. Why the 3 - i? And arg is a usize; why are you trying to initialize it to a field of TypeArray?

The error I get executing this code rightly complains about the 3 - i, since when i == 0 then we get TypeArray[3] which is out of range. Also, the struct here captures the current value of TypeArray, which is partially undefined – I think you probably want to reference Collection.Types instead, but again, I’m not sure what you’re trying to do exactly.

Sorry, I didn’t explain myself very well and didn’t realize the mistakes, I was typing on a phone, this is what I meant:

pub const Collection: type = blk: {
    var TypeArray: [3]type = undefined;

    for (&TypeArray, 0..) |*T, i| {
        T.* = struct {
            arg: usize,
            pub const Index = i;
            pub fn func() TypeArray[3 - i - 1] {
                return .{ .arg = TypeArray[3 - i - 1].Index };
            }
        };
    }

    const final = TypeArray;
    break :blk struct {
        pub const Types = final;
    };
};

comptime {
    @compileLog(Collection.Types[0].func());
}

What I meant to convey is just that I’m defining the return type of func based on the elements of TypeArray. Here’s the actual place I’m doing it in my library. I realize that TypeArray is partially undefined, but that’s the only way I can think of implementing this sort of behavior and it did work up until now. I’m assuming it only resolved the return type after Types was filled, when the actual function was called.

No worries! The issue you’re running into here is the undefined-ness – this is a change I forgot to mention in the notes for the recent PR, but when a type captures a comptime var, it basically just captures its current value. That’s my bad for not mentioning it; the change was actually made as a simplification, but I think it’s good to prevent weird behavior where the value depends on when the function is first referenced.

However, you’re right that the reference is lazily resolved once the function is called, so rather than using TypeArray, you can directly reference Collection.Types. Changing both of the TypeArray references in the struct to Collection.Types makes your code work as you expect.

You also don’t need final here, since you’re copying the comptime var value rather than taking a reference to it. So, this works:

pub const Collection: type = blk: {
    var TypeArray: [3]type = undefined;

    for (&TypeArray, 0..) |*T, i| {
        T.* = struct {
            arg: usize,
            pub const Index = i;
            pub fn func() Collection.Types[3 - i - 1] {
                return .{ .arg = Collection.Types[3 - i - 1].Index };
            }
        };
    }

    break :blk struct {
        pub const Types = TypeArray;
    };
};

comptime {
    @compileLog(Collection.Types[0].func());
}

I hope this answers your question?

3 Likes

This is close to what I want! However in my actual case I don’t really have a global Collection variable, it actually gets returned by a function, so I can’t do Collection.Types, but I realized I can do a recursive call in this case, like this:

pub fn Collection(comptime len: usize) type {
    const ConsArray = blk: {
        var TypeArray: [len]type = undefined;
 
        for (&TypeArray, 0..) |*T, i| {
            T.* = struct {
                arg: usize,
                pub const Index = i;
                pub fn func() Collection(len).Types[len - i - 1] {
                    return .{ .arg = Collection(len).Types[len - i - 1].Index };
                }
            };
        }
        break :blk TypeArray;
    };
 
    return struct {
        pub const Types = ConsArray;
    };
}
 
comptime {           
    @compileLog(Collection(3).Types[0].func());                          
}

It seems a bit strange, but it’s actually okay because I assume the actual call to Collection gets memoized by the compiler. I’ve done the equivalent in my library code and it seems to work now, thank you! I almost thought my use case was too esoteric to actually be supported by Zig :sweat_smile:

1 Like