How to construct a tuple at compile time using a for loop?

The following code does not work:

    comptime {
        var t = .{};
        for (.{ 92, "walrus", false }) |item| {
            t = t ++ .{item};
        }
    }
src/stdx.zig:830:15: error: expected type '@TypeOf(.{})', found 'struct{comptime comptime_int = 92}'
        t = t ++ .{item};
            ~~^~~~~~~~~~

Is there some nice way to make it work? I think I can make it work using recursion, but I’d rather write a simple loop, if possible!

If tuple type is known, following:

const std = @import("std");
	
pub fn main() !void {
    const t = comptime t: {
        const t0 = .{92, "walrus", false};
        var t1: @TypeOf(t0) = undefined;

        for (t0, 0..) |item, i| {
            t1[i] = item;
        }
        break:t t1;
    };
    std.debug.print("Tuple: {}\n", .{t});
}

Result:

$ zig build run
Tuple: { 92, { 119, 97, 108, 114, 117, 115 }, false }
2 Likes

But this would require the source elements to already be in a tuple (or struct) to fill in t1, no?


(The code below doesn’t compile. :frowning:) I haven’t quite gotten this to work, but I think it’s closer to what you need (and, eerily close to std.meta.Tuple(), though that only creates a tuple with the requires types?):

const std = @import("std");

fn itoa(comptime value: anytype) [:0]const u8 {
    comptime var s: [:0]const u8 = "";
    comptime var n = value;
    if (n == 0) {
        s = s ++ .{'0'};
    } else {
        while (n != 0) {
            s = s ++ .{'0' + (n % 10)};
            n = n / 10;
        }
    }
    return s;
}

fn tuplify(comptime fields: anytype) type {
    comptime var tuple_fields: []const std.builtin.Type.StructField = &.{};
    inline for (&fields) |*f| {
        const T = @TypeOf(f);
        const field: std.builtin.Type.StructField = .{
            .name = itoa(tuple_fields.len),
            .type = @TypeOf(T),
            .default_value = @as(?*const anyopaque, @ptrCast(@alignCast(@constCast(f)))),
            .is_comptime = true,
            .alignment = if (@sizeOf(T) > 0) @alignOf(T) else 0,
        };
        tuple_fields = tuple_fields ++ &[1]std.builtin.Type.StructField{field};
    }

    return @Type(.{
        .@"struct" = .{
            .is_tuple = true,
            .layout = .auto,
            .decls = &.{},
            .fields = tuple_fields,
        },
    });
}

pub fn main() !void {
    const t = tuplify(.{ 92, "walrus", false }){};
    std.debug.print("t: {any}\n", .{t});
}

In short, set up the anonymous struct, mark it as a tuple, give each new field a name that corresponds to the index, and add the StructFields one by one in a loop, then reify it at the end with @Type(). The default_value fields should make sure that default-initializing the tuple so constructed fills it with the right values.

However, this gives me the following error:

main.zig:34:12: error: comptime dereference requires 'type' to have a well-defined layout
    return @Type(.{
           ^~~~~

and I’m not sure why. :man_shrugging:

Ah, well, there were multiple problems with the previous version. Here’s a working version:

$ cat main.zig 
const std = @import("std");

fn itoa(comptime value: anytype) [:0]const u8 {
    const T = @TypeOf(value);
    if (comptime @typeInfo(T) != .int and @typeInfo(T) != .comptime_int) {
        @compileError("invalid type: expected integer or comptime_int, found " ++ @typeName(T));
    }

    comptime var s: [:0]const u8 = "";
    comptime var n = value;
    if (n == 0) {
        s = s ++ .{'0'};
    } else {
        inline while (n != 0) {
            s = .{'0' + (n % 10)} ++ s;
            n = n / 10;
        }
    }
    return s;
}

test "itoa" {
    try std.testing.expectEqualSlices(u8, "0", itoa(0));
    try std.testing.expectEqualSlices(u8, "1234567890", itoa(1234567890));
}

fn tuplify(comptime fields: anytype) type {
    comptime var tuple_fields: []const std.builtin.Type.StructField = &.{};
    inline for (fields) |*f| {
        const T = @TypeOf(f.*);
        tuple_fields = tuple_fields ++ &[1]std.builtin.Type.StructField{
            .{
                .name = itoa(tuple_fields.len),
                .type = T,
                .default_value = @as(?*const anyopaque, @ptrCast(@alignCast(@constCast(f)))),
                .is_comptime = @typeInfo(T) == .comptime_int or @typeInfo(T) == .comptime_float,
                .alignment = if (@sizeOf(T) > 0) @alignOf(T) else 0,
            },
        };
    }

    return @Type(.{
        .@"struct" = .{
            .is_tuple = true,
            .layout = .auto,
            .decls = &.{},
            .fields = tuple_fields,
        },
    });
}

test "tuplify" {
    try std.testing.expectEqualDeep(.{}, tuplify(&.{}){});
    try std.testing.expectEqualDeep(.{@as(u32, 92)}, tuplify(&.{@as(u32, 92)}){});
    try std.testing.expectEqualDeep(.{ 92, "walrus", false }, tuplify(&.{ 92, "walrus", false }){});

    const sentineled: [:0]const u8 = "hello, world!";
    try std.testing.expectEqualDeep(.{sentineled}, tuplify(&.{sentineled}){});
}

pub fn main() !void {
    const t: tuplify(&.{ 92, "walrus", false }) = .{};
    std.debug.print("t: {any}\n", .{t});
}

$ zig test main.zig
All 2 tests passed.

$ zig run main.zig
t: { 92, { 119, 97, 108, 114, 117, 115 }, false }

I’ll share a little secret with you… here’s a dirty trick for weakly-typed comptime:

comptime {
    const init_val = .{};
    var cur_ptr: *const anyopaque = &init_val;
    var CurTy = @TypeOf(init_val);
    for (.{ 92, "walrus", false }) |item| {
        const cur_val = @as(*const CurTy, @alignCast(@ptrCast(cur_ptr))).*;
        const new_val = cur_val ++ .{item};
        cur_ptr = &new_val;
        CurTy = @TypeOf(new_val);
    }
    const final = @as(*const CurTy, @alignCast(@ptrCast(cur_ptr))).*;
    @compileLog(final);
}

Note that the init_val is necessary due to a minor compiler bug which writing const ptr: *const anyopaque = &.{} exposes (although perhaps it’s good for that to not work…).

6 Likes