ziglang / zig

General-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.
https://ziglang.org
MIT License
34.79k stars 2.54k forks source link

Add new builtin: @unrollArgumentTuple #8048

Open ikskuh opened 3 years ago

ikskuh commented 3 years ago

Add new builtin: @unrollArgumentTuple

Semantics

fn @unrollArgumentTuple(comptime function: anytype) anytype

This builtin accepts any function that has the following requirements met:

The builtin returns a new function that:

Usage Example

fn allocateWithLogImpl(logger: Logger, args: tuple { *std.mem.Allocator, len: usize }) ![]u8
{
    logger.write("before allocation");
    defer logger.write("after allocation");

    return args[0].alloc(u8, args[1]);
}

const allocateWithLog = @unrollArgumentTuple(allocateWithLogImpl);

test {
    var buffer =  try allocateWithLog(Logger{}, std.testing.allocator, 16);
    ....
}

Implementation Example

fn foo(a: u32, vals: tuple{i32, f32}) void { … }

const fooUnrolled = @unrollArgumentTuple(foo);

will be unrolled into

fn fooUnrolled(a: u32, @"0": i32, @"1": f32} void {
  @call(.{ .modifier = always_inline }, foo, .{ a, .{ @"0", @"1" } });
}

Use Case

This builtin allows to create nicer comptime code which replaces some generic public functions.

Before: foo(.{ a, b, c}) After: foo(a, b, c)

Tuples were introduced primarily to create a replacement for variadic arguments but are now also used a lot in comptime code to create computable argument lists. This proposal allows to properly instantiate those computed APIs with actual argument list and reduces the amount of confusing error messages and error stack length when a wrong argument is passed to such a function.

Alternative

This behaviour is currently semi-ish implementable in user space by repeating a lot of code:

Extend to see implementation ```zig fn createCallWrapper(comptime FunctionType: type, comptime function: anytype) FunctionType { const fn_info = @typeInfo(FunctionType).Fn; const fn_args = fn_info.args; const R = fn_info.return_type orelse @compileError("Function must be non-generic"); comptime var A: [fn_args.len]type = undefined; inline for(A) |*t,i| { t.* = fn_args[i].arg_type orelse @compileError("Function must be non-generic"); } const Wrappers = struct { fn fn1(a0: A[0]) R { return function(a0,.{}); } fn fn2(a0: A[0], a1:A[1]) R { return function(a0,.{a1}); } fn fn3(a0: A[0], a1:A[1], a2:A[2]) R { return function(a0,.{a1,a2}); } fn fn4(a0: A[0], a1:A[1], a2:A[2], a3:A[3]) R { return function(a0,.{a1,a2,a3}); } fn fn5(a0: A[0], a1:A[1], a2:A[2], a3:A[3], a4:A[4]) R { return function(a0,.{a1,a2,a3,a4}); } fn fn6(a0: A[0], a1:A[1], a2:A[2], a3:A[3], a4:A[4], a5:A[5]) R { return function(a0,.{a1,a2,a3,a4,a5}); } fn fn7(a0: A[0], a1:A[1], a2:A[2], a3:A[3], a4:A[4], a5:A[5], a6:A[6]) R { return function(a0,.{a1,a2,a3,a4,a5,a6}); } fn fn8(a0: A[0], a1:A[1], a2:A[2], a3:A[3], a4:A[4], a5:A[5], a6:A[6], a7:A[7]) R { return function(a0,.{a1,a2,a3,a4,a5,a6,a7}); } fn fn9(a0: A[0], a1:A[1], a2:A[2], a3:A[3], a4:A[4], a5:A[5], a6:A[6], a7:A[7], a8:A[8]) R { return function(a0,.{a1,a2,a3,a4,a5,a6,a7,a8}); } fn fn10(a0: A[0], a1:A[1], a2:A[2], a3:A[3], a4:A[4], a5:A[5], a6:A[6], a7:A[7], a8:A[8], a9:A[9]) R { return function(a0,.{a1,a2,a3,a4,a5,a6,a7,a8,a9}); } fn fn11(a0: A[0], a1:A[1], a2:A[2], a3:A[3], a4:A[4], a5:A[5], a6:A[6], a7:A[7], a8:A[8], a9:A[9], a10:A[10]) R { return function(a0,.{a1,a2,a3,a4,a5,a6,a7,a8,a9,a10}); } fn fn12(a0: A[0], a1:A[1], a2:A[2], a3:A[3], a4:A[4], a5:A[5], a6:A[6], a7:A[7], a8:A[8], a9:A[9], a10:A[10], a11:A[11]) R { return function(a0,.{a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11}); } fn fn13(a0: A[0], a1:A[1], a2:A[2], a3:A[3], a4:A[4], a5:A[5], a6:A[6], a7:A[7], a8:A[8], a9:A[9], a10:A[10], a11:A[11], a12:A[12]) R { return function(a0,.{a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12}); } fn fn14(a0: A[0], a1:A[1], a2:A[2], a3:A[3], a4:A[4], a5:A[5], a6:A[6], a7:A[7], a8:A[8], a9:A[9], a10:A[10], a11:A[11], a12:A[12], a13:A[13]) R { return function(a0,.{a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13}); } fn fn15(a0: A[0], a1:A[1], a2:A[2], a3:A[3], a4:A[4], a5:A[5], a6:A[6], a7:A[7], a8:A[8], a9:A[9], a10:A[10], a11:A[11], a12:A[12], a13:A[13], a14:A[14]) R { return function(a0,.{a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14}); } fn fn16(a0: A[0], a1:A[1], a2:A[2], a3:A[3], a4:A[4], a5:A[5], a6:A[6], a7:A[7], a8:A[8], a9:A[9], a10:A[10], a11:A[11], a12:A[12], a13:A[13], a14:A[14], a15:A[15]) R { return function(a0,.{a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15}); } }; return switch(fn_args.len) { 1 => Wrappers.fn1, 2 => Wrappers.fn2, 3 => Wrappers.fn3, 4 => Wrappers.fn4, 5 => Wrappers.fn5, 6 => Wrappers.fn6, 7 => Wrappers.fn7, 8 => Wrappers.fn8, 9 => Wrappers.fn9, 10 => Wrappers.fn10, 11 => Wrappers.fn11, 12 => Wrappers.fn12, 13 => Wrappers.fn13, 14 => Wrappers.fn14, 15 => Wrappers.fn15, 16 => Wrappers.fn16, else => @compileError("Unsupported number of arguments!"), }; } ```
squeek502 commented 3 years ago

This builtin allows to create nicer comptime code which replaces some generic public functions.

Could you perhaps give an example of this (i.e. functions that currently exist in the std lib/elsewhere that could use @unrollArgumentTuple)?

I'm struggling to see why your example wouldn't just be written as

fn allocateWithLog(logger: Logger, allocator: *std.mem.Allocator, len: usize) ![]u8
ikskuh commented 3 years ago

See this example:

Usage:`` https://gist.github.com/MasterQ32/ef7187c56dcc195855390b234226d516#file-closure-zig-L22-L26

Implementation: https://gist.github.com/MasterQ32/ef7187c56dcc195855390b234226d516#file-closure-zig-L67-L69

With this proposal, you can just call _ = closed.invoke(1); instead of _ = closed.invoke(.{1});

This is just a small example though, there are better use cases, but it's a good one already :)

I'm struggling to see why your example wouldn't just be written as...

Yeah, this example was very artifical, just to be short. A real-world example would be some heavy comptime computation stuff being longer than the whole proposal itself