Returning anonymous struct literal of "unknown" (to the fn developer) type

Is there a way to write a function (I would call it function template, probably) that returns an anonymous struct whose exact composition (set of fields) depends on this function’s parameters?

Case in point - I have written a function (template) that takes a pointer to a struct (as anytype) and then walks its fields (inline for (tgt_type_info.Struct.fields) |tgt_fld|), filling them if it can (@field(ptr_tgt.*, tgt_fld.name) = ...).

I would like to be able to return another (anonymous) struct, where field names would correspond to the names of the fields of the struct I have received, but the types would be bool, or an enum.

Is it possible? What would be the return type of this function? How would I declare it? How would I add fields to my anonymous struct I would later return?

An alternative to this would be to expect yet another pointer to a struct from the function user, instead of returning a new struct, but this makes using my function harder.

Yes, it is possible.

The return type can be a function call that returns a type. See: zig-recover/src/recover.zig at 70fff6d6123a5625db50310119a02e4ae06fc73b · dimdin/zig-recover · GitHub

Prepare a typeinfo struct with the fields array and pass it to @Type()
At runtime call @field to set the actual value.

1 Like

I think I’ve got it.
Here’s my test code:

const std = @import("std");

pub fn main() void
{
    const Eggs = struct
    {
        comptime x: u8 = 123, // kinda like const

        tea: u32,
        lemon: f32,
        cake: []const u8
    };
    var e = Eggs { .tea = 10, .lemon = 0.2, .cake = "Yes, please!" };
    std.debug.print("\nBefore ham: e = {}\n", .{ e });
    const res = ham(&e);
    std.debug.print("After ham: e = {}\n", .{ e });
    std.debug.print("Modified fields: res = {}\n\n", .{ res });
}

/// Just mutate fields of arbitraty struct that `tgt_struct_ptr` points to,
/// and return the information on which fields were modified.
fn ham(tgt_struct_ptr: anytype) makeRetType(@TypeOf(tgt_struct_ptr))
{
    const tgt_struct_ptr_type_info = @typeInfo(@TypeOf(tgt_struct_ptr));
    if (tgt_struct_ptr_type_info != .Pointer or
        tgt_struct_ptr_type_info.Pointer.is_const or
        tgt_struct_ptr_type_info.Pointer.size != .One or
        @typeInfo(tgt_struct_ptr_type_info.Pointer.child) != .Struct)
        @compileError("`tgt_struct_ptr` is " ++ @typeName(@TypeOf(tgt_struct_ptr)) ++
                      " , expected a single-item pointer to a mutable (non-const) struct");

    var result =
    blk:
    {
        var res_val: makeRetType(@TypeOf(tgt_struct_ptr)) = undefined;
        inline for (@typeInfo(@TypeOf(res_val)).Struct.fields) |fld|
            @field(res_val, fld.name) = false;
        break :blk res_val;
    };

    inline for (@typeInfo(tgt_struct_ptr_type_info.Pointer.child).Struct.fields) |tgt_fld|
    {
        if (!tgt_fld.is_comptime)
            switch (@typeInfo(tgt_fld.type))
            {
                .Int =>
                {
                    @field(tgt_struct_ptr.*, tgt_fld.name) = 123;
                    @field(result, tgt_fld.name) = true;
                },
                .Float =>
                {
                    @field(tgt_struct_ptr.*, tgt_fld.name) = 1.23;
                    @field(result, tgt_fld.name) = true;
                },
                else => {}
            };
    }

    return result;
}

fn makeRetType(tgt_type: type) type
{
    const tgt_type_info = @typeInfo(tgt_type);
    if (tgt_type_info != .Pointer or
        tgt_type_info.Pointer.is_const or
        tgt_type_info.Pointer.size != .One or
        @typeInfo(tgt_type_info.Pointer.child) != .Struct)
        @compileError("fn makeRetType: `tgt_type` is " ++ @typeName(tgt_type) ++
                      " , expected a single-item pointer to a mutable (non-const) struct");

    const tgt_struct_type_info = @typeInfo(tgt_type_info.Pointer.child);
    const flds =
    blk:
    {
        var non_comptime_fld_count = 0; // Count non-comptime fields of the target struct
        for (tgt_struct_type_info.Struct.fields) |tgt_fld|
        {
            if (!tgt_fld.is_comptime)
                non_comptime_fld_count += 1;
        }

        var flds_values: [non_comptime_fld_count]std.builtin.Type.StructField = undefined;
        var i = 0; // comptime_int
        for (tgt_struct_type_info.Struct.fields) |tgt_fld|
        {
            if (!tgt_fld.is_comptime)
            {
                flds_values[i] =
                .{
                    .name = tgt_fld.name,
                    .type = bool,
                    .default_value = null,
                    .is_comptime = false,
                    .alignment = @alignOf(bool)
                };
                i += 1;
            }
        }
        break :blk flds_values;
    };

    return @Type(.{
                    .Struct =
                    .{
                        .layout = .Auto,
                        .backing_integer = null,
                        .fields = &flds,
                        .decls = &.{},
                        .is_tuple = false,
                    }
                 });
}

fn ham should be able to set the values of int and float fields of arbitrary structs passed to it (pointers to them), and return anonymous struct with bool fields with their names matching the names of the original struct, values indicating whether the field was modified or not (if not an integer or float).

1 Like

This is really good zig meta-exercise :slightly_smiling_face:
Meta-programming is difficult not only to write but also to read and maintain.
I have found that to achieve greatness you need to use as little meta as possible, ideally none at all.

2 Likes

I agree, in that writing meta-code takes time, and debugging sometimes makes me want to tear my hair out (@compileLog is your only friend).

Oh, and by the way, that was just an exercise before I transplant parts of it into more complex meta-code, which does recursive comptime struct mutation.

Writing meta code is hard, but then using it is really easy and sweet, at least it my case. Basically I’m writing code I wish someone else had written for me.

And in the end I’m glad I had to write this code, it really teaches you a thing or two. For example I didn’t know comptime fields in structs are a thing before I started this little sub-project.

1 Like

For the record, adding nested structs to the returned anonymous struct literal wasn’t as hard as I expected:

const std = @import("std");

pub fn main() void
{
    const Eggs = struct
    {
        const Ham = struct
        {
            const Milk = struct { cow: u8, soy: i8, almond: bool };

            bread: i16,
            butter: f32,
            nails: [2]i32,
            cream: Milk
        };

        comptime x: u8 = 123, // kinda like const
        tea: u32,
        lemon: f32,
        cake: []const u8,
        ham: Ham
    };
    var e = Eggs
    {
        .tea = 10, .lemon = 0.2, .cake = "Yes, please!",
        .ham =
        .{
            .bread = 1, .butter = 0.2, .nails = .{ -2, -3 },
            .cream = .{ .cow = 1, .soy = -1, .almond = true }
        }
    };
    std.debug.print("\nBefore mutateFields: e = {}\n\n", .{ e });
    const res = mutateFields(&e);
    std.debug.print("After mutateFields: e = {}\n\n", .{ e });
    std.debug.print("Modified fields: res = {}\n\n", .{ res });
    //_ = mutateFields(12); // Uncomment to test argument type protection in `mutateFields'`
}

/// Mutate integer and float fields of arbitraty struct that `tgt_struct_ptr` points to,
/// and return the information on which fields were modified.
fn mutateFields(tgt_struct_ptr: anytype)
    makeRetType(if (@typeInfo(@TypeOf(tgt_struct_ptr)) != .Pointer or
                    @typeInfo(@TypeOf(tgt_struct_ptr)).Pointer.is_const or
                    @typeInfo(@TypeOf(tgt_struct_ptr)).Pointer.size != .One or
                    @typeInfo(@typeInfo(@TypeOf(tgt_struct_ptr)).Pointer.child) != .Struct)
                    @compileError("fn mutateFields: `tgt_struct_ptr` is " ++ @typeName(@TypeOf(tgt_struct_ptr)) ++
                                  ", expected a single-item pointer to a mutable (non-const) struct")
                else
                    @typeInfo(@TypeOf(tgt_struct_ptr)).Pointer.child)
{
    const tgt_struct_ptr_type_info = @typeInfo(@TypeOf(tgt_struct_ptr));
    var result: makeRetType(tgt_struct_ptr_type_info.Pointer.child) = undefined;
    inline for (@typeInfo(tgt_struct_ptr_type_info.Pointer.child).Struct.fields) |tgt_fld|
    {
        if (!tgt_fld.is_comptime)
            switch (@typeInfo(tgt_fld.type))
            {
                .Int =>
                {
                    @field(tgt_struct_ptr.*, tgt_fld.name) = 123;
                    @field(result, tgt_fld.name) = true;
                },
                .Float =>
                {
                    @field(tgt_struct_ptr.*, tgt_fld.name) = 1.23;
                    @field(result, tgt_fld.name) = true;
                },
                .Struct =>
                    @field(result, tgt_fld.name) = mutateFields(&@field(tgt_struct_ptr.*, tgt_fld.name)),
                else => @field(result, tgt_fld.name) = false,
            };
    }
    return result;
}

/// Create a return type for `fn mutateFields` - an anonymous struct,
/// where field names are the same as in the `tgt_type` struct, but field type is `bool`.
/// Nested structs in `tgt_type` become nested structs in the returned type.
fn makeRetType(tgt_type: type) type
{
    const tgt_type_info = @typeInfo(tgt_type);
    if (tgt_type_info != .Struct)
        @compileError("fn makeRetType: `tgt_type` is " ++ @typeName(tgt_type) ++
                      " , expected a struct");
    const flds =
    blk:
    {
        var non_comptime_fld_count = 0; // Count non-comptime fields of the target struct
        for (tgt_type_info.Struct.fields) |tgt_fld|
        {
            if (!tgt_fld.is_comptime)
                non_comptime_fld_count += 1;
        }

        var flds_values: [non_comptime_fld_count]std.builtin.Type.StructField = undefined;
        var i = 0; // comptime_int
        for (tgt_type_info.Struct.fields) |tgt_fld|
        {
            if (!tgt_fld.is_comptime)
            {
                flds_values[i] =
                .{
                    .name = tgt_fld.name,
                    .type = if (@typeInfo(tgt_fld.type) == .Struct) makeRetType(tgt_fld.type) else bool,
                    .default_value = null,
                    .is_comptime = false,
                    .alignment = @alignOf(if (@typeInfo(tgt_fld.type) == .Struct) makeRetType(tgt_fld.type) else bool)
                };
                i += 1;
            }
        }
        break :blk flds_values;
    };

    return @Type(.{
                    .Struct =
                    .{
                        .layout = .Auto,
                        .backing_integer = null,
                        .fields = &flds,
                        .decls = &.{},
                        .is_tuple = false,
                    }
                 });
}