const std = @import("std"); const mach = @import("../main.zig"); const StringTable = @import("StringTable.zig"); const Graph = @import("graph.zig").Graph; /// An ID representing a mach object. This is an opaque identifier which effectively encodes: /// /// * An array index that can be used to O(1) lookup the actual data / struct fields of the object. /// * The generation (or 'version') of the object, enabling detecting use-after-object-delete in /// many (but not all) cases. /// * Which module the object came from, allowing looking up type information or the module name /// from ID alone. /// * Which list of objects in a module the object came from, allowing looking up type information /// or the object type name - which enables debugging and type safety when passing opaque IDs /// around. /// pub const ObjectID = u64; const ObjectTypeID = u16; const PackedObjectTypeID = packed struct(u16) { // 2^10 (1024) modules in an application module_name_id: u10, // 2^6 (64) lists of objects per module object_name_id: u6, }; pub const ObjectsOptions = struct { /// If set to true, Mach will track when fields are set using the setField/setAll /// methods using a bitset with one bit per field to indicate 'the field was set'. /// You can get this information by calling `.updated(.field_name)` /// Note that calling `.updated(.field_name) will also set the flag back to false. track_fields: bool = false, }; pub fn Objects(options: ObjectsOptions, comptime T: type) type { return struct { internal: struct { allocator: std.mem.Allocator, /// Mutex to be held when operating on these objects. /// TODO(object): replace with RwLock and update website docs to indicate this mu: std.Thread.Mutex = .{}, /// A registered ID indicating the type of objects being represented. This can be /// thought of as a hash of the module name + field name where this objects list is /// stored. type_id: ObjectTypeID, /// The actual object data data: std.MultiArrayList(T) = .{}, /// Whether a given slot in data[i] is dead or not dead: std.bit_set.DynamicBitSetUnmanaged = .{}, /// The current generation number of data[i], when data[i] becomes dead and then alive /// again, this number is incremented by one. generation: std.ArrayListUnmanaged(Generation) = .{}, /// The recycling bin which tells which data indices are dead and can be reused. recycling_bin: std.ArrayListUnmanaged(Index) = .{}, /// The number of objects that could not fit in the recycling bin and hence were thrown /// on the floor and forgotten about. This means there are dead items recorded by dead.set(index) /// which aren't in the recycling_bin, and the next call to new() may consider cleaning up. thrown_on_the_floor: u32 = 0, /// Global pointer to object relations graph graph: *Graph, /// A bitset used to track per-field changes. Only used if options.track_fields == true. updated: ?std.bit_set.DynamicBitSetUnmanaged = if (options.track_fields) .{} else null, }, pub const IsMachObjects = void; const Generation = u16; const Index = u32; const PackedID = packed struct(u64) { type_id: ObjectTypeID, generation: Generation, index: Index, }; pub const Slice = struct { index: Index, objs: *Objects(options, T), /// Same as Objects(T).set but doesn't employ safety checks pub fn set(objs: *@This(), id: ObjectID, value: T) void { const data = &objs.internal.data; const unpacked: PackedID = @bitCast(id); data.set(unpacked.index, value); } /// Same as Objects(T).get but doesn't employ safety checks pub fn get(objs: *@This(), id: ObjectID) ?T { const data = &objs.internal.data; const unpacked: PackedID = @bitCast(id); return data.get(unpacked.index); } /// Same as Objects(T).delete but doesn't employ safety checks pub fn delete(objs: *@This(), id: ObjectID) void { const dead = &objs.internal.dead; const recycling_bin = &objs.internal.recycling_bin; const unpacked: PackedID = @bitCast(id); if (recycling_bin.items.len < recycling_bin.capacity) { recycling_bin.appendAssumeCapacity(unpacked.index); } else objs.internal.thrown_on_the_floor += 1; dead.set(unpacked.index); } pub fn next(iter: *Slice) ?ObjectID { const dead = &iter.objs.internal.dead; const generation = &iter.objs.internal.generation; const num_objects = generation.items.len; while (true) { if (iter.index == num_objects) { iter.index = 0; return null; } defer iter.index += 1; if (!dead.isSet(iter.index)) return @bitCast(PackedID{ .type_id = iter.objs.internal.type_id, .generation = generation.items[iter.index], .index = iter.index, }); } } }; /// Tries to acquire the mutex without blocking the caller's thread. /// Returns `false` if the calling thread would have to block to acquire it. /// Otherwise, returns `true` and the caller should `unlock()` the Mutex to release it. pub fn tryLock(objs: *@This()) bool { return objs.internal.mu.tryLock(); } /// Acquires the mutex, blocking the caller's thread until it can. /// It is undefined behavior if the mutex is already held by the caller's thread. /// Once acquired, call `unlock()` on the Mutex to release it. pub fn lock(objs: *@This()) void { objs.internal.mu.lock(); } /// Releases the mutex which was previously acquired with `lock()` or `tryLock()`. /// It is undefined behavior if the mutex is unlocked from a different thread that it was locked from. pub fn unlock(objs: *@This()) void { objs.internal.mu.unlock(); } pub fn new(objs: *@This(), value: T) std.mem.Allocator.Error!ObjectID { const allocator = objs.internal.allocator; const data = &objs.internal.data; const dead = &objs.internal.dead; const generation = &objs.internal.generation; const recycling_bin = &objs.internal.recycling_bin; // The recycling bin should always be big enough, but we check at this point if 10% of // all objects have been thrown on the floor. If they have, we find them and grow the // recycling bin to fit them. if (objs.internal.thrown_on_the_floor >= (data.len / 10)) { var iter = dead.iterator(.{}); while (iter.next()) |index| try recycling_bin.append(allocator, @intCast(index)); objs.internal.thrown_on_the_floor = 0; } if (recycling_bin.popOrNull()) |index| { // Reuse a free slot from the recycling bin. dead.unset(index); const gen = generation.items[index] + 1; generation.items[index] = gen; return @bitCast(PackedID{ .type_id = objs.internal.type_id, .generation = gen, .index = index, }); } // Ensure we have space for the new object try data.ensureUnusedCapacity(allocator, 1); try dead.resize(allocator, data.capacity, true); try generation.ensureUnusedCapacity(allocator, 1); // If we are tracking fields, we need to resize the bitset to hold another object's fields if (objs.internal.updated) |*updated_fields| { try updated_fields.resize(allocator, data.capacity * @typeInfo(T).@"struct".fields.len, false); } const index = data.len; data.appendAssumeCapacity(value); dead.unset(index); generation.appendAssumeCapacity(0); return @bitCast(PackedID{ .type_id = objs.internal.type_id, .generation = 0, .index = @intCast(index), }); } /// Sets all fields of the given object to the given value. /// /// Unlike setAll(), this method does not respect any mach.Objects tracking /// options, so changes made to an object through this method will not be tracked. pub fn setValueRaw(objs: *@This(), id: ObjectID, value: T) void { const data = &objs.internal.data; const unpacked = objs.validateAndUnpack(id, "setValueRaw"); data.set(unpacked.index, value); } /// Sets all fields of the given object to the given value. /// /// Unlike setAllRaw, this method respects mach.Objects tracking /// and changes made to an object through this method will be tracked. pub fn setValue(objs: *@This(), id: ObjectID, value: T) void { const data = &objs.internal.data; const unpacked = objs.validateAndUnpack(id, "setValue"); data.set(unpacked.index, value); if (objs.internal.updated) |*updated_fields| { const updated_start = unpacked.index * @typeInfo(T).@"struct".fields.len; const updated_end = updated_start + @typeInfo(T).@"struct".fields.len; updated_fields.setRangeValue(.{ .start = @intCast(updated_start), .end = @intCast(updated_end) }, true); } } /// Sets a single field of the given object to the given value. /// /// Unlike set(), this method does not respect any mach.Objects tracking /// options, so changes made to an object through this method will not be tracked. pub fn setRaw(objs: *@This(), id: ObjectID, comptime field_name: std.meta.FieldEnum(T), value: std.meta.FieldType(T, field_name)) void { const data = &objs.internal.data; const unpacked = objs.validateAndUnpack(id, "setRaw"); var current = data.get(unpacked.index); @field(current, @tagName(field_name)) = value; data.set(unpacked.index, current); } /// Sets a single field of the given object to the given value. /// /// Unlike setAllRaw, this method respects mach.Objects tracking /// and changes made to an object through this method will be tracked. pub fn set(objs: *@This(), id: ObjectID, comptime field_name: std.meta.FieldEnum(T), value: std.meta.FieldType(T, field_name)) void { const data = &objs.internal.data; const unpacked = objs.validateAndUnpack(id, "set"); var current = data.get(unpacked.index); @field(current, @tagName(field_name)) = value; data.set(unpacked.index, current); if (options.track_fields) if (std.meta.fieldIndex(T, @tagName(field_name))) |field_index| if (objs.internal.updated) |*updated_fields| updated_fields.set(unpacked.index * @typeInfo(T).@"struct".fields.len + field_index); } /// Get a single field. pub fn get(objs: *@This(), id: ObjectID, comptime field_name: std.meta.FieldEnum(T)) std.meta.FieldType(T, field_name) { const data = &objs.internal.data; const unpacked = objs.validateAndUnpack(id, "get"); const d = data.get(unpacked.index); return @field(d, @tagName(field_name)); } /// Get all fields. pub fn getValue(objs: *@This(), id: ObjectID) T { const data = &objs.internal.data; const unpacked = objs.validateAndUnpack(id, "getValue"); return data.get(unpacked.index); } pub fn delete(objs: *@This(), id: ObjectID) void { const data = &objs.internal.data; const dead = &objs.internal.dead; const recycling_bin = &objs.internal.recycling_bin; const unpacked = objs.validateAndUnpack(id, "delete"); if (recycling_bin.items.len < recycling_bin.capacity) { recycling_bin.appendAssumeCapacity(unpacked.index); } else objs.internal.thrown_on_the_floor += 1; dead.set(unpacked.index); if (mach.is_debug) data.set(unpacked.index, undefined); } pub fn slice(objs: *@This()) Slice { return Slice{ .index = 0, .objs = objs, }; } // TODO: this doesn't type check currently, but it should (verify id is from this pool of objects.) fn validateAndUnpack(objs: *@This(), id: ObjectID, comptime fn_name: []const u8) PackedID { const dead = &objs.internal.dead; const generation = &objs.internal.generation; // TODO(object): decide whether to disable safety checks like this in some conditions, // e.g. in release builds const unpacked: PackedID = @bitCast(id); if (unpacked.generation != generation.items[unpacked.index]) { @panic("mach: " ++ fn_name ++ "() called with an object that is no longer valid"); } if (dead.isSet(unpacked.index)) { @panic("mach: " ++ fn_name ++ "() called on a dead object"); } return unpacked; } /// If options have tracking enabled, this returns true when the given field has been set using the set() /// or setAll() methods. A subsequent call to .updated() will return false until another set() or setAll() /// call is made. pub fn updated(objs: *@This(), id: ObjectID, field_name: anytype) bool { if (options.track_fields) { const unpacked = objs.validateAndUnpack(id, "updated"); if (std.meta.fieldIndex(T, @tagName(field_name))) |field_index| { if (objs.internal.updated) |*updated_fields| { const updated_index = unpacked.index * @typeInfo(T).@"struct".fields.len + field_index; const updated_value = updated_fields.isSet(updated_index); updated_fields.unset(updated_index); return updated_value; } } } return false; } /// Tells if the given object (which must be alive and valid) is from this pool of objects. pub fn is(objs: *const @This(), id: ObjectID) bool { const unpacked = objs.validateAndUnpack(id, "is"); return unpacked.type_id == objs.internal.type_id; } /// Get the parent of the child, or null. /// /// Object relations may cross the object-pool boundary; for example the parent or child of /// an object in this pool may not itself be in this pool. It might be from a different /// pool and a different type of object. pub fn getParent(objs: *@This(), id: ObjectID) !?ObjectID { return objs.internal.graph.getParent(objs.internal.allocator, id); } /// Set the parent of the child, or no-op if already the case. /// /// Object relations may cross the object-pool boundary; for example the parent or child of /// an object in this pool may not itself be in this pool. It might be from a different /// pool and a different type of object. pub fn setParent(objs: *@This(), id: ObjectID, parent: ?ObjectID) !void { try objs.internal.graph.setParent(objs.internal.allocator, id, parent orelse return objs.internal.graph.removeParent(objs.internal.allocator, id)); } /// Get the children of the parent; returning a results.items slice which is read-only. /// Call results.deinit() when you are done to return memory to the graph's memory pool for /// reuse later. /// /// Object relations may cross the object-pool boundary; for example the parent or child of /// an object in this pool may not itself be in this pool. It might be from a different /// pool and a different type of object. pub fn getChildren(objs: *@This(), id: ObjectID) !Graph.Results { return objs.internal.graph.getChildren(objs.internal.allocator, id); } /// Add the given child to the parent, or no-op if already the case. /// /// Object relations may cross the object-pool boundary; for example the parent or child of /// an object in this pool may not itself be in this pool. It might be from a different /// pool and a different type of object. pub fn addChild(objs: *@This(), id: ObjectID, child: ObjectID) !void { return objs.internal.graph.addChild(objs.internal.allocator, id, child); } /// Remove the given child from the parent, or no-op if not the case. /// /// Object relations may cross the object-pool boundary; for example the parent or child of /// an object in this pool may not itself be in this pool. It might be from a different /// pool and a different type of object. pub fn removeChild(objs: *@This(), id: ObjectID, child: ObjectID) !void { return objs.internal.graph.removeChild(objs.internal.allocator, id, child); } }; } /// Unique identifier for every module in the program, including those only known at runtime. pub const ModuleID = u32; /// Unique identifier for a function within a single module, including those only known at runtime. pub const ModuleFunctionID = u16; /// Unique identifier for a function within a module, including those only known at runtime. pub const FunctionID = struct { module_id: ModuleID, fn_id: ModuleFunctionID }; pub fn Mod(comptime M: type) type { return struct { pub const IsMachMod = void; pub const module_name = M.mach_module; pub const Module = M; id: ModFunctionIDs(M), _ctx: *anyopaque, _run: *const fn (ctx: *anyopaque, fn_id: FunctionID) void, pub fn run(r: *const @This(), fn_id: FunctionID) void { r._run(r._ctx, fn_id); } pub fn call(r: *const @This(), comptime f: ModuleFunctionName2(M)) void { const fn_id = @field(r.id, @tagName(f)); r.run(fn_id); } }; } pub fn ModFunctionIDs(comptime Module: type) type { var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{}; for (Module.mach_systems) |fn_name| { fields = fields ++ [_]std.builtin.Type.StructField{.{ .name = @tagName(fn_name), .type = FunctionID, .default_value = null, .is_comptime = false, .alignment = @alignOf(FunctionID), }}; } return @Type(.{ .@"struct" = .{ .layout = .auto, .is_tuple = false, .fields = fields, .decls = &[_]std.builtin.Type.Declaration{}, }, }); } /// Enum describing all declarations for a given comptime-known module. // TODO: unify with ModuleFunctionName fn ModuleFunctionName2(comptime M: type) type { validate(M); var enum_fields: []const std.builtin.Type.EnumField = &[0]std.builtin.Type.EnumField{}; var i: u32 = 0; inline for (M.mach_systems) |fn_tag| { // TODO: verify decls are Fn or mach.schedule() decl enum_fields = enum_fields ++ [_]std.builtin.Type.EnumField{.{ .name = @tagName(fn_tag), .value = i }}; i += 1; } return @Type(.{ .@"enum" = .{ .tag_type = if (enum_fields.len > 0) std.math.IntFittingRange(0, enum_fields.len - 1) else u0, .fields = enum_fields, .decls = &[_]std.builtin.Type.Declaration{}, .is_exhaustive = true, }, }); } pub fn Modules(module_lists: anytype) type { inline for (moduleTuple(module_lists)) |module| { validate(module); } return struct { /// All modules pub const modules = moduleTuple(module_lists); /// Enum describing every module name compiled into the program. pub const ModuleName = NameEnum(modules); mods: ModulesByName(modules), module_names: StringTable = .{}, object_names: StringTable = .{}, graph: Graph, /// Enum describing all declarations for a given comptime-known module. fn ModuleFunctionName(comptime module_name: ModuleName) type { const module = @field(ModuleTypesByName(modules){}, @tagName(module_name)); validate(module); var enum_fields: []const std.builtin.Type.EnumField = &[0]std.builtin.Type.EnumField{}; var i: u32 = 0; inline for (module.mach_systems) |fn_tag| { // TODO: verify decls are Fn or mach.schedule() decl enum_fields = enum_fields ++ [_]std.builtin.Type.EnumField{.{ .name = @tagName(fn_tag), .value = i }}; i += 1; } return @Type(.{ .@"enum" = .{ .tag_type = if (enum_fields.len > 0) std.math.IntFittingRange(0, enum_fields.len - 1) else u0, .fields = enum_fields, .decls = &[_]std.builtin.Type.Declaration{}, .is_exhaustive = true, }, }); } pub fn init(m: *@This(), allocator: std.mem.Allocator) (std.mem.Allocator.Error || std.Thread.SpawnError)!void { m.* = .{ .mods = undefined, .graph = undefined, }; try m.graph.init(allocator, .{ // TODO(object): measured preallocations .queue_size = 32, .nodes_size = 32, .num_result_lists = 8, .result_list_size = 8, }); // TODO(object): errdefer release allocations made in this loop inline for (@typeInfo(@TypeOf(m.mods)).@"struct".fields) |field| { // TODO(objects): module-state-init const Mod2 = @TypeOf(@field(m.mods, field.name)); var mod: Mod2 = undefined; const module_name_id = try m.module_names.indexOrPut(allocator, @tagName(Mod2.mach_module)); inline for (@typeInfo(@TypeOf(mod)).@"struct".fields) |mod_field| { if (@typeInfo(mod_field.type) == .@"struct" and @hasDecl(mod_field.type, "IsMachObjects")) { const object_name_id = try m.module_names.indexOrPut(allocator, mod_field.name); // TODO: use packed struct(TypeID) here. Same thing, just get the type from central location const object_type_id: u16 = @bitCast(PackedObjectTypeID{ .module_name_id = @intCast(module_name_id), .object_name_id = @intCast(object_name_id), }); @field(mod, mod_field.name).internal = .{ .allocator = allocator, .type_id = object_type_id, .graph = &m.graph, }; } } @field(m.mods, field.name) = mod; } } pub fn deinit(m: *@This(), allocator: std.mem.Allocator) void { m.graph.deinit(allocator); // TODO: remainder of deinit } pub fn Module(module_tag_or_type: anytype) type { const module_name: ModuleName = blk: { if (@typeInfo(@TypeOf(module_tag_or_type)) == .enum_literal or @typeInfo(@TypeOf(module_tag_or_type)) == .@"enum") break :blk @as(ModuleName, module_tag_or_type); validate(module_tag_or_type); break :blk module_tag_or_type.mach_module; }; const module = @field(ModuleTypesByName(modules){}, @tagName(module_name)); validate(module); return struct { mods: *ModulesByName(modules), modules: *Modules(module_lists), pub const mod_name: ModuleName = module_name; pub fn getFunction(fn_name: ModuleFunctionName(mod_name)) FunctionID { return .{ .module_id = @intFromEnum(mod_name), .fn_id = @intFromEnum(fn_name), }; } pub fn run( m: *const @This(), comptime fn_name: ModuleFunctionName(module_name), ) void { const debug_name = @tagName(module_name) ++ "." ++ @tagName(fn_name); const f = @field(module, @tagName(fn_name)); const F = @TypeOf(f); if (@typeInfo(F) == .@"struct" and @typeInfo(F).@"struct".is_tuple) { // Run a list of functions instead of a single function // TODO: verify this is a mach.schedule() decl if (module_name != .app) @compileLog(module_name); inline for (f) |schedule_entry| { // TODO: unify with Modules(modules).get(M) const callMod: Module(schedule_entry.@"0") = .{ .mods = m.mods, .modules = m.modules }; const callFn = @as(ModuleFunctionName(@TypeOf(callMod).mod_name), schedule_entry.@"1"); callMod.run(callFn); } return; } // Inject arguments var args: std.meta.ArgsTuple(F) = undefined; outer: inline for (@typeInfo(std.meta.ArgsTuple(F)).@"struct".fields) |arg| { if (@typeInfo(arg.type) == .pointer and @typeInfo(std.meta.Child(arg.type)) == .@"struct" and comptime isValid(std.meta.Child(arg.type))) { // *Module argument // TODO: better error if @field(m.mods, ...) fails ("module not registered") @field(args, arg.name) = &@field(m.mods, @tagName(std.meta.Child(arg.type).mach_module)); continue :outer; } if (@typeInfo(arg.type) == .@"struct" and @hasDecl(arg.type, "IsMachMod")) { const M = arg.type.Module; var mv: Mod(M) = .{ .id = undefined, ._ctx = m.modules, ._run = (struct { pub fn run(ctx: *anyopaque, fn_id: FunctionID) void { const modules2: *Modules(module_lists) = @ptrCast(@alignCast(ctx)); modules2.callDynamic(fn_id); } }).run, }; inline for (M.mach_systems) |m_fn_name| { @field(mv.id, @tagName(m_fn_name)) = Module(M).getFunction(m_fn_name); } @field(args, arg.name) = mv; continue :outer; } @compileError("mach: function " ++ debug_name ++ " has an invalid argument(" ++ arg.name ++ ") type: " ++ @typeName(arg.type)); } const Ret = @typeInfo(F).@"fn".return_type orelse void; switch (@typeInfo(Ret)) { // TODO: define error handling of runnable functions .error_union => @call(.auto, f, args) catch |err| std.debug.panic("error: {s}", .{@errorName(err)}), else => @call(.auto, f, args), } } }; } pub fn get(m: *@This(), module_tag_or_type: anytype) Module(module_tag_or_type) { return .{ .mods = &m.mods, .modules = m }; } pub fn callDynamic(m: *@This(), f: FunctionID) void { const module_name: ModuleName = @enumFromInt(f.module_id); switch (module_name) { inline else => |mod_name| { const module_fn_name: ModuleFunctionName(mod_name) = @enumFromInt(f.fn_id); const mod: Module(mod_name) = .{ .mods = &m.mods, .modules = m }; const module = @field(ModuleTypesByName(modules){}, @tagName(mod_name)); validate(module); switch (module_fn_name) { inline else => |fn_name| mod.run(fn_name), } }, } } }; } /// Validates that the given struct is a Mach module. fn validate(comptime module: anytype) void { if (!@hasDecl(module, "mach_module")) @compileError("mach: invalid module, missing `pub const mach_module = .foo_name;` declaration: " ++ @typeName(@TypeOf(module))); if (@typeInfo(@TypeOf(module.mach_module)) != .enum_literal) @compileError("mach: invalid module, expected `pub const mach_module = .foo_name;` declaration, found: " ++ @typeName(@TypeOf(module.mach_module))); } fn isValid(comptime module: anytype) bool { if (!@hasDecl(module, "mach_module")) return false; if (@typeInfo(@TypeOf(module.mach_module)) != .enum_literal) return false; return true; } /// Given a tuple of Mach module structs, returns an enum which has every possible comptime-known /// module name. fn NameEnum(comptime mods: anytype) type { var enum_fields: []const std.builtin.Type.EnumField = &[0]std.builtin.Type.EnumField{}; for (mods, 0..) |module, i| { validate(module); enum_fields = enum_fields ++ [_]std.builtin.Type.EnumField{.{ .name = @tagName(module.mach_module), .value = i }}; } return @Type(.{ .@"enum" = .{ .tag_type = std.math.IntFittingRange(0, enum_fields.len - 1), .fields = enum_fields, .decls = &[_]std.builtin.Type.Declaration{}, .is_exhaustive = true, }, }); } /// Given a tuple of module structs or module struct tuples: /// /// ``` /// .{ /// .{ Baz, .{ Bar, Foo, .{ Fam } }, Bar }, /// Foo, /// Bam, /// .{ Foo, Bam }, /// } /// ``` /// /// Returns a flat tuple, deduplicated: /// /// .{ Baz, Bar, Foo, Fam, Bar, Bam } /// fn moduleTuple(comptime tuple: anytype) ModuleTuple(tuple) { return ModuleTuple(tuple){}; } /// Type-returning variant of merge() fn ModuleTuple(comptime tuple: anytype) type { if (@typeInfo(@TypeOf(tuple)) != .@"struct" or !@typeInfo(@TypeOf(tuple)).@"struct".is_tuple) { @compileError("Expected to find a tuple, found: " ++ @typeName(@TypeOf(tuple))); } var tuple_fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{}; loop: inline for (tuple) |elem| { if (@typeInfo(@TypeOf(elem)) == .type and @typeInfo(elem) == .@"struct") { // Struct type validate(elem); for (tuple_fields) |field| if (@as(*const type, @ptrCast(field.default_value.?)).* == elem) continue :loop; var num_buf: [128]u8 = undefined; tuple_fields = tuple_fields ++ [_]std.builtin.Type.StructField{.{ .name = std.fmt.bufPrintZ(&num_buf, "{d}", .{tuple_fields.len}) catch unreachable, .type = type, .default_value = &elem, .is_comptime = false, .alignment = if (@sizeOf(elem) > 0) @alignOf(elem) else 0, }}; } else if (@typeInfo(@TypeOf(elem)) == .@"struct" and @typeInfo(@TypeOf(elem)).@"struct".is_tuple) { // Nested tuple inline for (moduleTuple(elem)) |nested| { validate(nested); for (tuple_fields) |field| if (@as(*const type, @ptrCast(field.default_value.?)).* == nested) continue :loop; var num_buf: [128]u8 = undefined; tuple_fields = tuple_fields ++ [_]std.builtin.Type.StructField{.{ .name = std.fmt.bufPrintZ(&num_buf, "{d}", .{tuple_fields.len}) catch unreachable, .type = type, .default_value = &nested, .is_comptime = false, .alignment = if (@sizeOf(nested) > 0) @alignOf(nested) else 0, }}; } } else { @compileError("Expected to find a tuple or struct type, found: " ++ @typeName(@TypeOf(elem))); } } return @Type(.{ .@"struct" = .{ .is_tuple = true, .layout = .auto, .decls = &.{}, .fields = tuple_fields, }, }); } /// Given .{Foo, Bar, Baz} Mach modules, returns .{.foo = Foo, .bar = Bar, .baz = Baz} with field /// names corresponding to each module's `pub const mach_module = .foo;` name. fn ModuleTypesByName(comptime modules: anytype) type { var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{}; for (modules) |M| { fields = fields ++ [_]std.builtin.Type.StructField{.{ .name = @tagName(M.mach_module), .type = type, .default_value = &M, .is_comptime = true, .alignment = @alignOf(type), }}; } return @Type(.{ .@"struct" = .{ .layout = .auto, .is_tuple = false, .fields = fields, .decls = &[_]std.builtin.Type.Declaration{}, }, }); } /// Given .{Foo, Bar, Baz} Mach modules, returns .{.foo: Foo = undefined, .bar: Bar = undefined, .baz: Baz = undefined} /// with field names corresponding to each module's `pub const mach_module = .foo;` name, and each Foo type. fn ModulesByName(comptime modules: anytype) type { var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{}; for (modules) |M| { fields = fields ++ [_]std.builtin.Type.StructField{.{ .name = @tagName(M.mach_module), .type = M, .default_value = &@as(M, undefined), .is_comptime = false, .alignment = @alignOf(M), }}; } return @Type(.{ .@"struct" = .{ .layout = .auto, .is_tuple = false, .fields = fields, .decls = &[_]std.builtin.Type.Declaration{}, }, }); }