Zig: Add @Tuple

Created on 2 Mar 2020  路  4Comments  路  Source: ziglang/zig

This is a proposal to add a builtin function that returns a tuple type.
Proposed signature:

fn @Tuple([]type) type

This would be a useful feature for metaprogramming purposes.
More specifically, it would be helpful in metaprograms that generate functions that forward arguments using @call, as it could be used to generate non-generic function pointers that accept tuple types calculated at comptime.
Currently, passing a tuple requires the argument to be 'var' and forces the function to be generic.

Here is an example usecase from the std.interface PR:

// Here, we are generating a type erased version of a method  
const args = @typeInfo(fn_decl.fn_type).Fn.args;

// [...] Omitted code, compute our_cc, Return, CurrSelfType, is_const

// Actual function generation  
// Currently, there is no way to avoid the following switch statement.  

return switch (args.len) {
    1 => struct {
        fn impl(self_ptr: CurrSelfType) Return {
            const self = if (is_const) constSelfPtrAs(self_ptr, ImplT) else selfPtrAs(self_ptr, ImplT);
            const f = @field(self, name);

            return @call(if (our_cc == .Async) .{ .modifier = .async_kw } else .{ .modifier = .always_inline }, f, .{});
        }
    }.impl,
    2 => struct {
        fn impl(self_ptr: CurrSelfType, arg: args[1].arg_type.?) Return {
            const self = if (is_const) constSelfPtrAs(self_ptr, ImplT) else selfPtrAs(self_ptr, ImplT);
            const f = @field(self, name);

            return @call(if (our_cc == .Async) .{ .modifier = .async_kw } else .{ .modifier = .always_inline }, f, .{arg});
        }
    }.impl,
    // [...] Omitted 3 to 6
    else => @compileError("Unsupported number of arguments, please provide a manually written vtable."),
};

// With this proposal  
var arg_type_arr: [args.len - 1]type = undefined;
for (args[1..]) |arg, i| {
    arg_type_arr[i] = arg.arg_type.?;
}

const ArgPackType = @Tuple(arg_type_arr[0..]);

// Call site of generated function also changes from @call(..., .{self, a1,..., aN}) to @call(..., .{self, .{a1, ..., aN} }); 
return struct {
    fn impl(self_ptr: CurrSelfType, args: ArgPackType) Return {
        const self = if (is_const) constSelfPtrAs(self_ptr, ImplT) else selfPtrAs(self_ptr, ImplT);
        const f = @field(self, name);

        return @call(if (our_cc == .Async) .{ .modifier = .async_kw } else .{ .modifier = .always_inline }, f, args);
    }
}.impl;

proposal

Most helpful comment

This is actually achievable in userspace.
Thanks to @MasterQ32 for helping me get this working for runtime tuples as well

const std = @import("std");

// Create a tuple with one runtime-typed field 
fn UniTuple(comptime T: type) type {
    const Hack = struct {
        var forced_runtime: T = undefined;
    };
    return @TypeOf( .{ Hack.forced_runtime } );
}

/// Types should be an iterable of types
fn Tuple(comptime types: var) type {
    const H = struct {
        value: var,
    };
    var empty_tuple = .{};

    var container = H{
        .value = empty_tuple,
    };

    for (types) |T| {
        container.value = container.value ++ UniTuple(T){ .@"0" = undefined };
    }
    return @TypeOf(container.value);
}

pub fn main() !void
{
    const T = Tuple(.{ i32, []const u8 });
    var t : T = .{
        .@"0" = 42, 
        .@"1" = "Hello, World!"
    };
    std.debug.print("t = {}\n", .{ t });
}

I will try applying this to the usecase I mentioned in the opening comment (in interface.zig) tomorrow.

All 4 comments

This is also useful for generating stack variables in code that generates calls to user functions. Use Case: I'm building an ECS that accepts a comptime-known function and generates a loop that inlines the function into the body. The parameters to the function are obtained from parallel arrays which need to be fetched by type, which has a runtime cost. Ideally I would be able to cache these arrays in stack variables, but without a way to generate stack vars based on the input function signature there isn't a typesafe way to do it. I've been getting around it for now by generating an on-stack [N][*]u8, casting the slice pointers to [*]u8, and then restoring them to the proper type at the call site. So without a builtin like @Tuple, you need this:

    const fn_info = @typeInfo(user_fn).Fn;
    const args = fn_info.args;
    comptime var types: [args.len]type = undefined;
    comptime var array_types: [args.len]type = undefined;
    var arrays: [args.len][*]u8 = undefined;
    inline for (args) |arg, i| {
        const ArgType = arg.arg_type.?;
        types[i] = ArgType;
        array_types[i] = ArgToArrayType(ArgType); // *T -> [*]T, *const T -> [*]const T, etc
        arrays[i] = @ptrCast([*]u8, chunk.fetchSlice(ArgType).ptr);
    }

    for (chunk.range()) |_,i| {
        switch (args.len) {
            1 => @call(.{ .modifier = .always_inline }, user_fn, .{ @ptrCast(array_types[0], @alignCast(@alignOf(array_types[0].child, arrays[0]))[i] }),
            2 => @call(.{ .modifier = .always_inline }, user_fn, .{
                @ptrCast(array_types[0], @alignCast(@alignOf(array_types[0].child, arrays[0]))[i],
                @ptrCast(array_types[1], @alignCast(@alignOf(array_types[1].child, arrays[1]))[i],
            }),
            3 => @call(.{ .modifier = .always_inline }, user_fn, .{
                @ptrCast(array_types[0], @alignCast(@alignOf(array_types[0].child, arrays[0]))[i],
                @ptrCast(array_types[1], @alignCast(@alignOf(array_types[1].child, arrays[1]))[i],
                @ptrCast(array_types[2], @alignCast(@alignOf(array_types[2].child, arrays[2]))[i],
            }),
            // etc
        }
    }

Alternatively, I could move the switch out to contain both the arg fetching and call statement. That approach would provide type safety but would also mean repeating myself 10 times (assuming 10 args is the limit).
But with @Tuple, I could get rid of the type erasure in addition to the switch statement:

    const fn_info = @typeInfo(user_fn).Fn;
    const args = fnInfo.args;

    comptime var types: [args.len]type = undefined;
    comptime var slice_types: [args.len]type = undefined;
    comptime for (args) |arg, i| {
        const ArgType = arg.arg_type.?;
        types[i] = ArgType;
        slice_types[i] = ArgToSliceType(ArgType); // *T -> []T, *const T -> []const T, etc
    }

    const SliceTuple = @Tuple(slice_types);
    const ArgTuple = @Tuple(types);

    var slices: SliceTuple = undefined;
    inline for (types) |ArgType, i| {
        slices[i] = chunk.fetchSlice(ArgType);
    }

    for (range(chunk.len)) |_,chunk_i| {
        var callArgs: ArgTuple = undefined;
        inline for (types) |_, arg_i| {
            callArgs[arg_i] = slices[arg_i][chunk_i];
        }
        @call(.{ .modifier = always_inline }, user_fn, callArgs);
    }

This ends up being a few more lines, but importantly, it now undergoes full type checking. It also gets rid of the call switch, though in this simple case that could be solved alternately by adding call_tuple: ?type to builtin.TypeInfo.Fn.

This is actually achievable in userspace.
Thanks to @MasterQ32 for helping me get this working for runtime tuples as well

const std = @import("std");

// Create a tuple with one runtime-typed field 
fn UniTuple(comptime T: type) type {
    const Hack = struct {
        var forced_runtime: T = undefined;
    };
    return @TypeOf( .{ Hack.forced_runtime } );
}

/// Types should be an iterable of types
fn Tuple(comptime types: var) type {
    const H = struct {
        value: var,
    };
    var empty_tuple = .{};

    var container = H{
        .value = empty_tuple,
    };

    for (types) |T| {
        container.value = container.value ++ UniTuple(T){ .@"0" = undefined };
    }
    return @TypeOf(container.value);
}

pub fn main() !void
{
    const T = Tuple(.{ i32, []const u8 });
    var t : T = .{
        .@"0" = 42, 
        .@"1" = "Hello, World!"
    };
    std.debug.print("t = {}\n", .{ t });
}

I will try applying this to the usecase I mentioned in the opening comment (in interface.zig) tomorrow.

This is now possible in userspace with @Type(.Struct).

Was this page helpful?
0 / 5 - 0 ratings