Comptime code to create a tuple from an array

I am trying to call a method based on runtime values. Consider this method:

fn add(a: i32, b: i32) i32 {
    return a + b;
}

And this runtime known array:

const args = // a runtime value that contains .{ "add", "1", "2" };

I want to call add with 1 and 2 as parameter. I know how to define a set of function like this:

const named_functions = .{
  .{ .name = "add", .func = add },
  .{ .name = "foo", .func = foo },
  // ...
};

And then a method makeCall would go though the tuple, select the function based on the name, convert the arguments depending on the @typeInfo(FuncType).@"fn".params and use @call. But @call expects a tuple to make the call:

@call(.auto, "add", .{ 1, 2 });

And this is where I get a little stuck. How to go from a dynamically known array to a tuple?

I could switch on the number of argument (pseudo code):

switch (@typeInfo(FuncType).@"fn".params.len) {
  1 => @call(auto, name, .{ convert(paramType, args[1]) }),
  2 => @call(auto, name, .{ convert(paramType, args[1]), convert(paramType, args[2])}),
  ...
}

But isn’t there a way to create a tuple at compile time with the content of an array? Something like (pseudo-code):

  const max_nb_args = getMaxNbArgs(named_functions);
  inline for (max_nb_args) |nb_arg| {
    if (nb_arg == @typeInfo(FuncType).@"fn".params.len) {
      const params = convertArgs(@typeInfo(FuncType).@"fn".params, args);
      @call(auto, name, .{ ...param1 }), // magic
    }
  }

Is there a zig construct to do that?

Maybe I can get inspiration from zig code here:

  var args: std.meta.ArgsTuple(Encode) = undefined;
  inline for (&args, @typeInfo(Encode).@"fn".params, 1..instruction.encode.len) |*arg, param, encode_index|
      arg.* = zonCast(param.type.?, instruction.encode[encode_index], symbols);
  return @call(.auto, encode, args);

I am confused on where you intend to get the runtime arguments for your function invocation from, I don’t think your post explicitly mentions this?

When you have a function type you can use std.meta.ArgsTuple to extract the corresponding tuple type for the parameters, that you can use to call that function.

Once you have that tuple type you need to create an instance of that type.

Does this mean you have something like an array of int parameters and want to turn that into a tuple instance?

You can do that by creating code that first creates the tuple type and then creates an instance of that tuple type. Here is an example of something like that:

const std = @import("std");

fn BuildTupleFromArray(comptime Array: type) type {
    const Element = std.meta.Elem(Array);
    const len = @typeInfo(Array).array.len;
    const types_array: [len]type = @splat(Element);
    return std.meta.Tuple(&types_array);
}

fn buildTupleFromArray(array: anytype) BuildTupleFromArray(@TypeOf(array)) {
    var a: BuildTupleFromArray(@TypeOf(array)) = undefined;
    inline for (&array, 0..) |value, i| {
        a[i] = value;
    }
    return a;
}

pub fn calc(a: i32, b: i32, c: i32) i32 {
    return a * 2 + b * b + c * 94 + 13;
}

pub fn main() !void {
    const tuple = buildTupleFromArray([_]i32{ 123, -379, 22 });
    std.debug.print("tuple: {}\n", .{tuple});

    const result = @call(.auto, calc, tuple);
    std.debug.print("result: {}\n", .{result});
}

Let me know if there is something else you are trying to accomplish.


You also might want to look at some older topics that do interesting/related things with tuples:

1 Like

Ha. This question is right up my alley. I had the same need when building ZigJR, i.e. making calls to handler functions with arbitrary JSON values as function parameters at runtime.

Look at json_call.zig for the details. See json_call_tests.zig for the usages. json_call.ArgsTupleType() is the function building the tuple type from a function parameter array in comptime. The various callOnXX() are the functions making the calls with runtime values.

1 Like