Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add @Tuple #4607

Closed
alexnask opened this issue Mar 2, 2020 · 4 comments
Closed

Add @Tuple #4607

alexnask opened this issue Mar 2, 2020 · 4 comments
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@alexnask
Copy link
Contributor

alexnask commented Mar 2, 2020

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;
@fengb
Copy link
Contributor

fengb commented Mar 2, 2020

#4335

@andrewrk andrewrk added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Mar 3, 2020
@andrewrk andrewrk added this to the 0.7.0 milestone Mar 3, 2020
@SpexGuy
Copy link
Contributor

SpexGuy commented Mar 3, 2020

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.

@alexnask
Copy link
Contributor Author

alexnask commented Jun 21, 2020

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.

@alexnask
Copy link
Contributor Author

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

alexnask added a commit that referenced this issue Sep 29, 2020
Implements std.meta.Tuple(), implements #4607 in userland.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

No branches or pull requests

4 participants