Meta-programming and dependency loop

One of the things I love about Zig is the comptime meta-programming that I can do.

In my case, I am going through a structure and finding all the structs with a particular structure and creating an enum and an array of function pointers that allow me to access those at compile time. (In the actual code, this comes from several @imports.) This particular way of doing things is working around 2 zig bugs (dependency loops with. simple pointers, and compile-time objects containing pointer fields). Here is code that is extracted and heavily simplified from the actual system.

Note that without the test this code works in my system. The only thing I can see is that the references in that code to Enum and functions are at compile time, but when I put a reference in a test, it moves them to runtime.

const std = @import("std");
const Tf = struct {
  tfn: ThreadedFn.Fn,

};
pub const ThreadedFn = struct {
    f: Fn,
    pub const Fn = *const fn (foo: *Tf) u64;
};
const structures = .{
    struct {
        pub const bar = struct {
            pub fn threadedFn(foo: *Tf) u64 {
                return @intFromPtr(foo);
            }
        };
    },
};
//comptime {@compileLog(structures[6].asThunk);}
fn declsCount() usize {
    comptime var count = 0;
    for (structures) |structure| {
        count += @typeInfo(structure).@"struct".decls.len;
    }
    return count;
}
const EnumSort = struct {
    field: *const std.builtin.Type.Declaration,
    threadedFn: ThreadedFn.Fn,
};
const enumAndFunctions =
    blk: {
        @setEvalBranchQuota(100000);
        var array: [declsCount()]EnumSort = undefined;
        var n = 0;
        for (structures) |structure| {
            const decls = @typeInfo(structure).@"struct".decls;
            for (decls) |decl| {
                const ds = @field(structure, decl.name);
                switch (@typeInfo(@TypeOf(ds))) {
                    .comptime_int, .int, .@"fn", .array => {},
                    else => {
                        if (@hasDecl(ds, "threadedFn")) {
                            array[n] = .{ .field = &decl, .threadedFn = @field(ds, "threadedFn") };
                            n += 1;
                        }
                    },
                }
            }
        }
        const enums = array[0..n];
        var fields = @typeInfo(enum {}).@"enum".fields;
        for (enums, 0..) |d, i| {
            fields = fields ++ [_]std.builtin.Type.EnumField{.{
                .name = d.field.name,
                .value = i,
            }};
        }
        const arraySize = enums.len;
        var arrayFns: [arraySize]ThreadedFn.Fn = undefined;
        var arrayNames: [arraySize][]u8 = undefined;
        for (enums, 0..) |eb, index| {
            arrayFns[index] = eb.threadedFn;
            arrayNames[index] = eb.field.name;
        }

        break :blk .{ @Type(.{ .@"enum" = .{
            .tag_type = usize,
            .is_exhaustive = false,
            .fields = fields,
            .decls = &.{},
        } }), arrayFns, arrayNames };
    };

pub const Enum = enumAndFunctions[0];
const functions = enumAndFunctions[1];
pub const names = enumAndFunctions[2];

test "print threadedFns" {
    std.debug.print("Threaded Functions:\n", .{});
    for (names) |name| {
        std.debug.print("{s:<25}", .{name});
    }
    // originally had this, but introduced names array in attempt to resolve depndency loop
    // for (@typeInfo(Enum).@"enum".fields) |field| {
    //     std.debug.print("{s:<25}", .{field.name});
    // }
    std.debug.print("\n", .{});
    // std.debug.print("git version: {s}\n", .{ config.git_version })
}

(note, the names field was added later, trying to work around the dependency loop.)

I’m not sure if it will help in this case, but you might want to try an inline for to generate the prints at compile time:

test "print threadedFns" {
    std.debug.print("Threaded Functions:\n", .{});
    inline for (names) |name| {
        std.debug.print("{s:<25}", .{name});
    }
    std.debug.print("\n", .{});
    // std.debug.print("git version: {s}\n", .{ config.git_version })
}

I believe that the inline expands the loop at compiletime and should avoid the issue, but someone more knowledgable can correct me.

Good suggestion, but unfortunately didn’t solve the problem, even in this case.

I also need access to the names and functions at run-time.

You code will compile with these changes:

// Original:
tfn: ThreadedFn.Fn
// Revised:
tfn: *const fn (foo: *Tf) u64

// ...

// Original:
var arrayNames: [arraySize][]u8
// Revised:
var arrayNames: [arraySize][]const u8

Zig doesn’t check for dependency loops in declarations that aren’t referenced. The test referenced names, whose construction references ThreadedFn.Fn, which has a dependency loop. Because of Zig’s lazy compilation, non-referenced code ‘working’ (not causing a compile error) means very little.

2 Likes