Search code examples
metaprogrammingquickcheckzig

Build variable length arguments array for @call


I've recently started learning Zig. As a little project I wanted to implement a small QuickCheck [1] style helper library for writing randomized tests.

However, I can't figure out how to write a generic way to call a function with an arbitrary number of arguments.

Here's a simplified version that can test functions with two arguments:

const std    = @import("std");
const Prng   = std.rand.DefaultPrng;
const Random = std.rand.Random;
const expect = std.testing.expect;

// the thing we want to test
fn some_property(a: u64, b: u64) !void {
    var tmp: u64 = undefined;
    var c1 = @addWithOverflow(u64, a, b, &tmp);
    var c2 = @addWithOverflow(u64, a, b, &tmp);

    expect(c1 == c2);
}

// helper for generating random arguments for the function under test
fn gen(comptime T: ?type, rnd: Random) (T orelse undefined) {
    switch (T orelse undefined) {
        u64  => return rnd.int(u64),
        f64  => return rnd.float(f64),
        else => @compileError("unsupported type"),
    }
}

/// tests if 'property' holds.
fn for_all(property: anytype) !void {
  var rnd = Prng.init(0);

  const arg_types = @typeInfo(@TypeOf(property)).Fn.args;

  var i: usize = 0;
  while (i < 100) {
    var a = gen(arg_types[0].arg_type, rnd.random());
    var b = gen(arg_types[1].arg_type, rnd.random());

    var args = .{a, b}; // <-- how do I build args for functions with any number of arguments?

    try @call(.{}, property, args);

    i += 1;
  }
}

test "test" {
  try for_all(some_property);
}

I've tried a few different things, but I can't figure out how to get the above code to work for functions with any number of arguments.

Things I've tried:

  • Make args an array and fill it with an inline for loop. Doesn't work since []anytype is not a valid type.
  • Use a bit of comptime magic to build a struct type whose fields hold the arguments for @call. This hits a TODO in the compiler: error: TODO: struct args.
  • Write generic functions that return an appropriate argument tuple call. I don't really like this one, since you need one function for every arity you want to support. But it doesn't seem to work anyway since antype is not a valid return type.

I'm on Zig 0.9.1.

Any insight would be appreciated.

[1] https://hackage.haskell.org/package/QuickCheck


Solution

  • This can be done with std.meta.ArgsTuple (defined in this file of the zig standard library)

        const Args = std.meta.ArgsTuple(@TypeOf(property));
    
        var i: usize = 0;
        while (i < 1000) : (i += 1) {
            var args: Args = undefined;
            inline for (std.meta.fields(Args)) |field, index| {
                args[index] = gen(field.field_type, rnd.random());
            }
    
            try @call(.{}, property, args);
        }
    

    The way this works internally is it constructs a tuple type with @Type(). We can then fill it with values and use it to call the function.