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.