How to make Zig reflection skip @complieError?

When I use a module generated by translate-c, I want to use reflection to obtain declarations of specific types in the module for generating Zig code. However, the translation result usually contains some macro definitions that Zig cannot translate. They are emitted as compile errors, and when my code iterates over them, it directly triggers a compile error. How can I avoid this?

const std = @import("std");
const c = @import("c-mod");

pub fn main(init: std.process.Init) !void {
    const decls = @typeInfo(c).@"struct".decl_names;
    @setEvalBranchQuota(decls.len * decls.len);

    const outfile = std.Io.File.stdout();

    var buffer: [64]u8 = undefined;
    var writer = outfile.writer(init.io, &buffer);
    const w = &writer.interface;

    inline for (decls) |decl| {
        const F = @TypeOf(@field(c, decl));
        try w.print("{any}\n", .{F});
    }

    try w.flush();
}

tool_find_all_funcs
└─ run exe find_all_funcs
   └─ compile exe find_all_funcs debug native 1 errors
c\windows.zig:3003:20: error: unable to resolve typedef child type
pub const __v8bf = @compileError("unable to resolve typedef child type");
                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
referenced by:
    main: tools\find_all_funcs.zig:15:27
    callMain [inlined]: C:\Users\lenovo\.local\share\zvm\0.17.0\lib\std\start.zig:827:30
    WinStartup: C:\Users\lenovo\.local\share\zvm\0.17.0\lib\std\start.zig:576:17
    2 reference(s) hidden; use '-freference-trace=5' to see all references

Declarations are only evaluated when you try to use them, even @typeInfo wont do it (that’s why it only gives you its name). So you can just guard its’ usage by checking its name (at comptime)

But I need to iterate over all items and filter which ones are the content I need by their type; this cannot avoid accessing the @complieError item.

Zig’s lazy evaluation will ignore code that is comptime known to be not reached.

So if you check, at comptime, the name and skip it then Zig wont evaluate the declaration and you wont get a compile error, assuming nothing else uses it.

And a reminder that functions wont be called at comptime unless they are forced to by comptime or other context.

Is there any simpler way? The header file I’m translating (Windows.h) is very large . I checked, and there are more than 1,000 occurrences of @compileError alone. Handling it manually would be too much work.

Yes, this is impossible. You can’t ascertain a decl’s type without using @field() and therefore evaluating it.
Your only hope is to filter based on the decl name to skip the ones that compile-error.
My suggestion is using std.mem.containsAtLeast functions.

It will always be based on the name, but there may be a smarter way to filter depending on what you are doing.

There is also the option of modifying the translated c>zig to remove or replace the compile errors

Last time I checked, translating Windows.h is basically a nonstarter, since the macro it uses to choose between A and W functions based on whether the UNICODE macro is defined is very MSVC-specific.
In the past, the workaround for this has usually been to use zigwin32 instead.

Zig explicitly does not have plans to provide a solution to this problem, see mlugg comment here : Proposal: Prevent @typeInfo and @TypeOf from triggering compile errors · Issue #6620 · ziglang/zig · GitHub

Like tholmes said, if there are any libraries you depend on and need to introspect their decls, you are SOL and need to find an alternate library, or rewrite it yourself maybe automate creating a decl wrapper that omits decls that become assigned to @compileError under particular configurations.

I hope that Zig libraries develop a convention where we never assign pub decls to @compileError but rather prefer to assign to void or some kind of InvalidType type or to functions that contain a @compileError, otherwise the scalable solution to this is… ???:person_shrugging::person_shrugging::person_shrugging:???

I’ve thought of an approach that will at least work: use a program to find which symbols cause compilation errors and generate them into an exclusion name set, so that they can be directly excluded without too much effort.

const std = @import("std");
const api = @import("win32api");

const template_start =
    \\const std = @import("std");
    \\const c = @import("win32api");
    \\
    \\pub fn main() !void {
    \\    const set: std.StaticStringMap(void) = .initComptime(.{
    \\
;
const template_end =
    \\    });
    \\
    \\    const decls = @typeInfo(c).@"struct".decl_names;
    \\    @setEvalBranchQuota(decls.len * decls.len);
    \\
    \\    inline for (decls) |decl| {
    \\        if (comptime !set.has(decl) and std.mem.find(u8, decl, "_") == null) {
    \\            const info = @typeInfo(@TypeOf(@field(c, decl)));
    \\            _ = info;
    \\        }
    \\    }
    \\}
    \\
;

pub fn main(init: std.process.Init) !void {
    const io = init.io;
    const gpa = init.gpa;

    var zigfile = try std.Io.Dir.cwd().createFile(
        io,
        "tools/compileError.zig",
        .{},
    );
    defer zigfile.close(io);

    var outfile = try std.Io.Dir.cwd().createFile(io, ".zig-cache/tmp.txt", .{ .read = true });
    defer outfile.close(io);

    var buffer: [64]u8 = undefined;
    var writer = zigfile.writer(io, &buffer);
    const w = &writer.interface;

    var compile_error_set: std.StringHashMap(void) = .init(gpa);
    defer compile_error_set.deinit();

    while (true) {
        try writer.seekTo(0);
        try w.writeAll(template_start);
        var iter = compile_error_set.iterator();
        while (iter.next()) |entry| {
            try w.print("        .{{ \"{s}\", {{}} }},\n", .{entry.key_ptr.*});
        }
        try w.writeAll(template_end);
        try w.flush();

        var run = try std.process.spawn(io, .{
            .argv = &.{ "zig", "build", "tool_compileError", "--color", "off", "-freference-trace=100" },
            .stderr = .{ .file = outfile },
        });
        const term = try run.wait(io);
        if (term.success()) break;

        var rbuffer: [64]u8 = undefined;
        var reader = outfile.reader(io, &rbuffer);
        const r = &reader.interface;

        const output = try r.readAllocAll(init.arena.allocator(), try reader.getSize());

        const prefix = "pub const ";
        if (std.mem.find(u8, output, prefix)) |i| {
            if (std.mem.findPos(u8, output, i + prefix.len, " ")) |j| {
                const ident = output[i + prefix.len .. j];
                if (!compile_error_set.contains(ident)) {
                    try compile_error_set.put(ident, {});
                    std.debug.print("{s}\n", .{ident});
                }
            }
        }
        const prefix2 = "referenced by:";
        if (std.mem.find(u8, output, prefix2)) |begin| {
            const content = output[begin + prefix2.len ..];
            var split = std.mem.splitSequence(u8, content, "\n    ");
            while (split.next()) |line| {
                if (std.mem.findAny(u8, line, ": ")) |index| {
                    const ident = line[0..index];
                    if (std.mem.eql(u8, ident, "main"))
                        break;
                    try compile_error_set.put(ident, {});
                    std.debug.print("{s}\n", .{ident});
                }
            }
        }
    }
}

You also could use the std.zig.Ast parser and then walk the AST to find the names of declarations that are directly assigned to @compileError if all the compile errors don’t use any conditional build logic / comptime if’s that might be faster than spawning lots of build processes.

I think a good starting point for reading how you can use zig’s Ast tooling is by reading the implementation of the zig fmt command, for example there is also already support for generating a modified version of source code by using Ast.Render.Fixups.

So overall that strategy can be described as translate-c → generator-run-step → actual finished module source code, with everything changed/removed that isn’t wanted from the translate-c output.

This is a WIP generator for raylib bindings that should illustrate the idea (some missing imports), but it should give an outline of the main logic how you can use the pre-existing parsing and re-rendering with fixups to generate modified zig source code (0.16.0):

const std = @import("std");
const Io = std.Io;
const Allocator = std.mem.Allocator;
const Ast = std.zig.Ast;
const assert = std.debug.assert;

const color: std.zig.Color = .auto;

const options = @import("options");

const raylib = @embedFile("raylib");
// const rcamera = @embedFile("rcamera");
const raymath = @embedFile("raymath");
const rlgl = @embedFile("rlgl");

// const modules: [][:0]const u8 = .{ "raylib", "rcamera", "raymath", "rlgl" };
// const data: [][:0]const u8 = .{ raylib, rcamera, raymath, rlgl };
const modules: []const [:0]const u8 = &.{ "raylib", "raymath", "rlgl" };
const string_data: []const [:0]const u8 = &.{ raylib, raymath, rlgl };

fn note(comptime format: []const u8, args: anytype) void {
    if (options.verbose) std.log.info(format, args);
}

pub fn main(init: std.process.Init) !void {
    const io = init.io;
    const gpa = init.gpa;
    const arena: Allocator = init.arena.allocator();

    note("options:\n{s}", .{@embedFile("options")});

    // Accessing command line arguments:
    const args = try init.minimal.args.toSlice(arena);
    if (options.verbose) for (args) |arg| {
        note("arg: {s}", .{arg});
    };

    const output_file_path = args[2];
    var output_file = Io.Dir.cwd().createFile(io, output_file_path, .{}) catch |err| {
        fatal("unable to open '{s}': {s}", .{ output_file_path, @errorName(err) });
    };
    defer output_file.close(io);

    const module = args[1];
    var match = false;
    inline for (modules, string_data) |m, d| {
        if (std.mem.eql(u8, m, module)) {
            try process(io, gpa, arena, m, d, output_file);
            match = true;
        }
    }
    if (!match) {
        fatal("unable to generate module '{s}': available options {any}", .{ module, modules });
    }

    note("generated output to file: {s}", .{output_file_path});
    return std.process.cleanExit(io);
}

fn process(
    io: Io,
    gpa: Allocator,
    arena: Allocator,
    comptime module: [:0]const u8,
    data: [:0]const u8,
    output_file: Io.File,
) !void {
    var tree: Ast = try .parse(gpa, data, .zig);
    defer tree.deinit(gpa);

    if (tree.errors.len != 0) {
        std.zig.printAstErrorsToStderr(gpa, io, tree, "<translate c input for " ++ module ++ ">", color) catch {};
        std.process.exit(2);
    }

    const patched = try findAndApplyFixups(tree, gpa, arena);
    defer gpa.free(patched);

    try output_file.writeStreamingAll(io, patched);
}

const Walker = struct {
    const rules = @import("rules.zig");
    const Enum = @import("enums.zig").Enum;

    const Render = Ast.Render;
    const Container = enum {
        @"enum",
        tuple,
        other,
    };

    gpa: Allocator,
    arena: Allocator,
    tree: Ast,
    fixups: *Ast.Render.Fixups,
    enums: Enum.Map,

    fn deinit(w: *Walker) void {
        var it = w.enums.valueIterator();
        while (it.next()) |lst| lst.values.deinit(w.gpa);
        w.enums.deinit(w.gpa);
    }

    fn findFixups(w: *Walker) !void {
        try processRootDecls(w, w.tree.rootDecls());
        try generateEnums(w);
    }

    fn processRootDecls(w: *Walker, members: []const Ast.Node.Index) !void {
        const tree = w.tree;
        if (members.len == 0) return;
        const container: Container = for (members) |member| {
            if (tree.fullContainerField(member)) |field| if (!field.ast.tuple_like) break .other;
        } else .tuple;
        try processRootMember(w, container, members[0]);
        for (members[1..]) |member| {
            try processRootMember(w, container, member);
        }
    }

    fn processRootMember(w: *Walker, container: Container, decl: Ast.Node.Index) !void {
        _ = container;
        const tree = w.tree;
        const tag = tree.nodeTag(decl);

        // based on std.zig.Ast.Render.renderMember
        const Operations = union(enum) {
            abort,
            omit_node,

            find_name,
            check_name: Info,
            fixup: Info,

            const Info = struct {
                name: []const u8,
                fn_token: ?u32 = null,
            };
        };
        const op: Operations = .find_name;
        op: switch (op) {
            .abort => return,
            .omit_node => {
                try w.fixups.omit_nodes.put(w.gpa, decl, {});
                return;
            },
            .find_name => {
                const fn_token, const name_token = tok: switch (tag) {
                    .simple_var_decl => {
                        const var_decl = tree.fullVarDecl(decl).?;
                        const name_token = var_decl.ast.mut_token + 1;
                        break :tok .{ null, name_token };
                    },
                    .fn_decl => {
                        const fn_proto_index = tree.nodeData(decl).node_and_node[0];
                        const fn_token = tree.nodeMainToken(fn_proto_index);
                        const name_token = fn_token + 1;
                        break :tok .{ fn_token, name_token };
                    },
                    .fn_proto_simple,
                    .fn_proto_multi,
                    .fn_proto_one,
                    .fn_proto,
                    => {
                        const fn_token = tree.nodeMainToken(decl);
                        const name_token = fn_token + 1;
                        break :tok .{ fn_token, name_token };
                    },
                    else => |t| {
                        note("find_name tag: {t}", .{t});
                        return;
                    },
                };

                assert(tree.tokenTag(name_token) == .identifier);
                const name = tree.tokenSlice(name_token);
                continue :op Operations{ .check_name = .{ .name = name, .fn_token = fn_token } };
            },
            .check_name => |info| {
                const name = info.name;
                for (rules.banned_starts) |start| {
                    if (std.mem.startsWith(u8, name, start)) {
                        if (rules.allowed_names.get(name) != null) continue :op .abort;
                        continue :op .omit_node;
                    }
                }
                if (rules.banned_names.get(name) != null) continue :op .omit_node;
                continue :op .{ .fixup = info };
            },
            .fixup => |info| {
                const name = info.name;
                switch (tag) {
                    .simple_var_decl => {
                        if (std.mem.endsWith(u8, name, "_H")) continue :op .omit_node;

                        const struct_ = "struct_";
                        if (std.mem.startsWith(u8, name, struct_)) {
                            const rest_name = name[struct_.len..];
                            if (rules.overrides.get(rest_name) != null) continue :op .omit_node;
                        }

                        const var_decl = tree.fullVarDecl(decl).?;
                        if (rules.overrides.get(name) != null) {
                            const node = var_decl.ast.init_node.unwrap().?;
                            const replaced = try std.fmt.allocPrint(w.arena, "@import(\"overrides\").{s}", .{name});
                            try w.fixups.replace_nodes_with_string.put(w.gpa, node, replaced);
                            // note("simple_var_decl: {s} replaced with: {s}", .{ name, replaced });
                            return;
                        }
                        if (rules.enums.get(name) != null) {
                            var gop = try w.enums.getOrPut(w.gpa, name);
                            if (!gop.found_existing) gop.value_ptr.* = .empty;
                            gop.value_ptr.node = var_decl.ast.init_node.unwrap().?;
                            continue :op .abort;
                        }
                        for (rules.enums.keys(), rules.enums.values()) |k, v| {
                            if (std.mem.startsWith(u8, name, v.prefix)) {
                                var gop = try w.enums.getOrPut(w.gpa, k);
                                if (!gop.found_existing) gop.value_ptr.* = .empty;
                                const suffix = name[v.prefix.len..];
                                try gop.value_ptr.values.append(w.gpa, .{ .name = suffix, .value = try w.getValue(var_decl) });
                                continue :op .omit_node;
                            }
                        }

                        note("simple_var_decl: {s}", .{name});
                    },
                    .fn_decl => {
                        // const fn_proto_index, const body_node = tree.nodeData(decl).node_and_node;
                        // const fn_token = tree.nodeMainToken(fn_proto_index);
                        // _ = body_node;
                        // TODO rewrite prototypes to use better types
                    },
                    .fn_proto_simple,
                    .fn_proto_multi,
                    .fn_proto_one,
                    .fn_proto,
                    => {
                        // const fn_token = tree.nodeMainToken(decl);
                        // TODO rewrite prototypes to use better types
                    },
                    else => |t| note("tag: {t}", .{t}),
                }
            },
        }
    }

    const GenEnum = struct {
        w: *Walker,
        n: []const u8,
        e: Enum,
        v: []Enum.Value,

        pub fn format(self: GenEnum, w: *std.Io.Writer) std.Io.Writer.Error!void {
            const n = self.n;
            try w.writeAll("enum(u32) {\n");
            if (self.e.add_invalid) try w.writeAll("    invalid = -1,\n");
            for (self.v) |x| try w.print("    {s} = {d},\n", .{ x.name, x.value });
            if (self.e.mode == .nonexhaustive) try w.writeAll("    _,\n");
            if (self.e.gen == .flags) {
                try w.print(
                    \\
                    \\    pub fn flags(f:[]const {[0]s}) {[0]s} {{
                    \\        var res = @import("std").mem.zeroes({[0]s});
                    \\        for(f) |x| res |= x;
                    \\        return res;
                    \\    }}
                    \\
                , .{n});
            }
            try w.writeAll("}");
        }
    };
    fn generateEnums(w: *Walker) !void {
        for (rules.enums.keys(), rules.enums.values()) |k, e| {
            const enum_info = w.enums.get(k) orelse continue;
            const g: GenEnum = .{ .w = w, .n = k, .e = e, .v = enum_info.values.items };
            const replaced = try std.fmt.allocPrint(w.arena, "{f}", .{g});
            try w.fixups.replace_nodes_with_string.put(w.gpa, enum_info.node.?, replaced);
        }
    }

    fn getValue(
        w: *Walker,
        var_decl: Ast.full.VarDecl,
    ) !u32 {
        const tree = w.tree;

        // code based on renderVarDeclWithoutFixups
        const last_component: enum {
            value,
            other,
        } = if (var_decl.ast.init_node != .none) .value else .other;

        assert(last_component == .value);
        const node = var_decl.ast.init_node.unwrap().?;

        switch (tree.nodeTag(node)) {
            .number_literal => {
                const token = tree.nodeMainToken(node);
                const value = std.fmt.parseInt(u32, tree.tokenSlice(token), 10) catch |err| std.debug.panic("unexpected error: {t}", .{err});
                return value;
            },
            else => |tag| {
                std.debug.panic("unsupported tag: {t}", .{tag});
            },
        }
    }
};

fn findAndApplyFixups(tree: Ast, gpa: Allocator, arena: Allocator) error{OutOfMemory}![]u8 {
    var fixups: Ast.Render.Fixups = .{};
    defer fixups.deinit(gpa);

    var walker: Walker = .{
        .gpa = gpa,
        .arena = arena,
        .tree = tree,
        .fixups = &fixups,
        .enums = .empty,
    };
    defer walker.deinit();
    try walker.findFixups();

    var aw: std.Io.Writer.Allocating = .init(gpa);
    defer aw.deinit();
    tree.render(gpa, &aw.writer, fixups) catch |err| switch (err) {
        error.WriteFailed, error.OutOfMemory => return error.OutOfMemory,
    };
    return aw.toOwnedSlice();
}

fn fatal(comptime format: []const u8, args: anytype) noreturn {
    std.debug.print(format, args);
    std.process.exit(1);
}

The missing imports just contain raylib specific rules and names to rename or to remove.

In your case I think you need to walk the tokens behind the = token of .simple_var_decls, I would look in the ast render code to reverse engineer which part renders @compileError expressions and then use that to identify the right declarations to add to the fixups to be omitted.