diff --git a/generator/generator.zig b/generator/generator.zig index cb9caec..a56648a 100644 --- a/generator/generator.zig +++ b/generator/generator.zig @@ -1,6 +1,7 @@ const std = @import("std"); const reg = @import("registry.zig"); const xml = @import("xml.zig"); +const renderRegistry = @import("render.zig").render; const parseXml = @import("registry/parse.zig").parseXml; const Allocator = std.mem.Allocator; const FeatureLevel = reg.FeatureLevel; @@ -167,10 +168,14 @@ pub const Generator = struct { self.registry_arena.deinit(); } - // Solve registry.declarations according to registry.extensions and registry.features + // Solve `registry.declarations` according to `registry.extensions` and `registry.features`. pub fn resolveDeclarations(self: *Generator) !void { var resolver = DeclarationResolver.init(self.gpa, &self.registry_arena.allocator, &self.registry); defer resolver.deinit(); try resolver.resolve(); } + + pub fn render(self: *Generator, out_stream: var) !void { + try renderRegistry(out_stream, self.gpa, &self.registry); + } }; diff --git a/generator/main.zig b/generator/main.zig index 20d5f39..fd85e94 100644 --- a/generator/main.zig +++ b/generator/main.zig @@ -122,16 +122,14 @@ pub fn main() !void { const spec = try xml.parse(allocator, source); defer spec.deinit(); - // const result = try parseXml(std.heap.page_allocator, spec.root); - // defer result.deinit(); - - // dumpRegistry(result.registry); - var gen = try vkgen.Generator.init(allocator, spec.root); defer gen.deinit(); try gen.resolveDeclarations(); + const stdout = std.io.getStdOut().writer(); + try gen.render(stdout); + std.debug.warn("Total declarations: {}\n", .{gen.registry.decls.len}); std.debug.warn("Total memory usage: {} KiB\n", .{@divTrunc(prof_alloc.max_usage, 1024)}); } @@ -140,3 +138,7 @@ test "main" { _ = @import("xml.zig"); _ = @import("registry/c-parse.zig"); } + +// TODO: Fix not all enums being parsed. +// TODO: Fix not all struct fields being marked as optional properly. +// TODO: Sort enum fields. diff --git a/generator/registry.zig b/generator/registry.zig index 1ed1cdc..4ab5a80 100644 --- a/generator/registry.zig +++ b/generator/registry.zig @@ -20,6 +20,7 @@ pub const DeclarationType = union(enum) { alias: Alias, foreign: Foreign, typedef: TypeInfo, + opaque, }; pub const Alias = struct { @@ -52,7 +53,6 @@ pub const TypeInfo = union(enum) { command_ptr: Command, pointer: Pointer, array: Array, - opaque, }; pub const Container = struct { @@ -72,7 +72,7 @@ pub const Enum = struct { bit_vector: i32, // Combined flags & some vendor IDs int: i32, alias: struct { - alias_name: []const u8, + name: []const u8, is_compat_alias: bool, } }; diff --git a/generator/registry/parse.zig b/generator/registry/parse.zig index 6904512..1654d52 100644 --- a/generator/registry/parse.zig +++ b/generator/registry/parse.zig @@ -151,7 +151,7 @@ fn parseBaseType(allocator: *Allocator, ty: *xml.Element) !registry.Declaration // macros, which is why this part is not built into the xml/c parser. return registry.Declaration{ .name = name, - .decl_type = .{.typedef = .{.opaque = {}}}, + .decl_type = .{.opaque = {}}, }; } } @@ -302,7 +302,7 @@ fn parseEnumField(field: *xml.Element) !registry.Enum.Field { } else if (field.getAttribute("bitpos")) |bitpos| { break :blk .{.bitpos = try std.fmt.parseInt(u5, bitpos, 10)}; } else if (field.getAttribute("alias")) |alias| { - break :blk .{.alias = .{.alias_name = alias, .is_compat_alias = is_compat_alias}}; + break :blk .{.alias = .{.name = alias, .is_compat_alias = is_compat_alias}}; } else { return error.InvalidRegistry; } diff --git a/generator/render.zig b/generator/render.zig index 4e45d62..78704df 100644 --- a/generator/render.zig +++ b/generator/render.zig @@ -1,3 +1,298 @@ const std = @import("std"); -const registry = @import("registry.zig"); -const Allocator = std.mem.Allocator, +const reg = @import("registry.zig"); +const util = @import("render/util.zig"); +const mem = std.mem; +const Allocator = mem.Allocator; + +const preamble = + \\const std = @import("std"); + \\ + ; + +const BuiltinType = struct { + c_name: []const u8, + zig_name: []const u8 +}; + +const builtin_types = [_]BuiltinType{ + .{.c_name = "void", .zig_name = @typeName(void)}, + .{.c_name = "char", .zig_name = @typeName(u8)}, + .{.c_name = "float", .zig_name = @typeName(f32)}, + .{.c_name = "double", .zig_name = @typeName(f64)}, + .{.c_name = "uint8_t", .zig_name = @typeName(u8)}, + .{.c_name = "uint16_t", .zig_name = @typeName(u16)}, + .{.c_name = "uint32_t", .zig_name = @typeName(u32)}, + .{.c_name = "uint64_t", .zig_name = @typeName(u64)}, + .{.c_name = "int32_t", .zig_name = @typeName(i32)}, + .{.c_name = "int64_t", .zig_name = @typeName(i64)}, + .{.c_name = "size_t", .zig_name = @typeName(usize)}, + .{.c_name = "int", .zig_name = @typeName(c_int)}, +}; + +fn eqlIgnoreCase(lhs: []const u8, rhs: []const u8) bool { + if (lhs.len != rhs.len) { + return false; + } + + for (lhs) |c, i| { + if (std.ascii.toLower(c) != std.ascii.toLower(rhs[i])) { + return false; + } + } + + return true; +} + +fn Renderer(comptime WriterType: type) type { + return struct { + const Self = @This(); + const WriteError = WriterType.Error; + const RenderTypeInfoError = WriteError || error { + OutOfMemory, + }; + + writer: WriterType, + allocator: *Allocator, + registry: *const reg.Registry, + id_renderer: util.IdRenderer, + + fn init(writer: WriterType, allocator: *Allocator, registry: *const reg.Registry) Self { + return .{ + .writer = writer, + .allocator = allocator, + .registry = registry, + .id_renderer = util.IdRenderer.init(allocator, registry.tags), + }; + } + + fn deinit(self: Self) void { + self.id_renderer.deinit(); + } + + fn writeIdentifier(self: Self, id: []const u8) !void { + try util.writeIdentifier(self.writer, id); + } + + fn writeIdentifierWithCase(self: *Self, case: util.CaseStyle, id: []const u8) !void { + try self.id_renderer.render(self.writer, case, id); + } + + fn extractEnumFieldName(self: Self, enum_name: []const u8, field_name: []const u8) ![]const u8 { + const tag = util.getAuthorTag(enum_name, self.registry.tags); + const adjusted_enum_name = if (tag) |name| + enum_name[0 .. enum_name.len - name.len] + else + enum_name; + + var enum_it = util.SegmentIterator.init(adjusted_enum_name); + var field_it = util.SegmentIterator.init(field_name); + + while (true) { + const rest = field_it.rest(); + const enum_segment = enum_it.next() orelse return rest; + const field_segment = field_it.next() orelse return error.FieldNameEqualsEnumName; + + if (!eqlIgnoreCase(enum_segment, field_segment)) { + return rest; + } + } + } + + fn render(self: *Self) !void { + try self.writer.writeAll(preamble); + + for (self.registry.decls) |decl| { + try self.renderDecl(decl); + } + } + + fn renderTypeInfo(self: *Self, type_info: reg.TypeInfo) RenderTypeInfoError!void { + switch (type_info) { + .name => |name| try self.renderTypeName(name), + .command_ptr => |command_ptr| try self.renderCommandPtr(command_ptr), + .pointer => |pointer| try self.renderPointer(pointer), + .array => |array| try self.renderArray(array), + } + } + + fn renderTypeName(self: *Self, name: []const u8) !void { + for (builtin_types) |builtin_type| { + if (mem.eql(u8, name, builtin_type.c_name)) { + try self.writer.writeAll(builtin_type.zig_name); + return; + } + } + + // TODO: Handle foreign types + + if (mem.startsWith(u8, name, "vk")) { + // Function type, always render with the exact same text for linking purposes. + try self.writeIdentifier(name); + return; + } else if (mem.startsWith(u8, name, "Vk")) { + // Type, strip namespace and write, as they are alreay in title case. + try self.writeIdentifier(name[2..]); + return; + } else if (mem.startsWith(u8, name, "PFN_vk")) { + // Function pointer type, render using same name for now + try self.writeIdentifier(name); + return; + } + + try self.writeIdentifier(name); + } + + fn renderCommandPtr(self: *Self, command_ptr: reg.Command) !void { + try self.writer.writeAll("?fn("); + for (command_ptr.params) |param| { + try self.writeIdentifierWithCase(.snake, param.name); + try self.writer.writeAll(": "); + try self.renderTypeInfo(param.param_type); + try self.writer.writeAll(", "); + } + try self.writer.writeAll(") "); + try self.renderTypeInfo(command_ptr.return_type.*); + } + + fn renderPointer(self: *Self, pointer: reg.Pointer) !void { + if (pointer.is_optional) { + try self.writer.writeByte('?'); + } + + switch (pointer.size) { + .one => try self.writer.writeByte('*'), + .many => try self.writer.writeAll("[*]"), + .zero_terminated => try self.writer.writeAll("[*:0]"), + } + + if (pointer.is_const) { + try self.writer.writeAll("const "); + } + + // Special case: void pointers + if (pointer.child.* == .name and mem.eql(u8, pointer.child.name, "void")) { + try self.writer.writeAll("c_void"); + } else { + try self.renderTypeInfo(pointer.child.*); + } + } + + fn renderArray(self: *Self, array: reg.Array) !void { + try self.writer.writeByte('['); + switch (array.size) { + .int => |size| try self.writer.print("{}", .{size}), + .alias => |alias| try self.writeIdentifier(alias[3..]), //TODO: Check proper VK_ prefix + } + try self.writer.writeByte(']'); + try self.renderTypeInfo(array.child.*); + } + + fn renderDecl(self: *Self, decl: reg.Declaration) !void { + switch (decl.decl_type) { + .container => |container| try self.renderContainer(decl.name, container), + .enumeration => |enumeration| try self.renderEnumeration(decl.name, enumeration), + .alias => |alias| try self.renderAlias(decl.name, alias), + .opaque => try self.renderOpaque(decl.name), + .typedef => |type_info| try self.renderTypedef(decl.name, type_info), + else => {}, // unhandled for now + } + } + + fn renderContainer(self: *Self, name: []const u8, container: reg.Container) !void { + try self.writer.writeAll("const "); + try self.renderTypeName(name); + try self.writer.writeAll(" = "); + + for (container.fields) |field| { + if (field.bits != null) { + try self.writer.writeAll("packed "); + break; + } + } else { + try self.writer.writeAll("extern "); + } + + if (container.is_union) { + try self.writer.writeAll("union {"); + } else { + try self.writer.writeAll("struct {"); + } + + for (container.fields) |field| { + try self.writeIdentifierWithCase(.snake, field.name); + try self.writer.writeAll(": "); + try self.renderTypeInfo(field.field_type); + // TODO: Generate struct defaults + // TODO: Deal with packed structs + try self.writer.writeAll(", "); + } + + try self.writer.writeAll("};\n"); + } + + fn renderEnumeration(self: *Self, name: []const u8, enumeration: reg.Enum) !void { + // TODO: Handle bitmasks + try self.writer.writeAll("const "); + try self.renderTypeName(name); + try self.writer.writeAll(" = extern enum {"); + + for (enumeration.fields) |field| { + if (field.value == .alias) { + continue; + } + + try self.writeIdentifierWithCase(.snake, try self.extractEnumFieldName(name, field.name)); + + switch (field.value) { + .int => |int| try self.writer.print("= {}, ", .{int}), + .bitpos => |pos| try self.writer.print(" = 1 << {}, ", .{pos}), + .bit_vector => |value| try self.writer.print(" = 0x{X}, ", .{value}), + .alias => unreachable, + } + } + + for (enumeration.fields) |field| { + if (field.value != .alias or field.value.alias.is_compat_alias) { + continue; + } + + try self.writer.writeAll("pub const "); + try self.writeIdentifierWithCase(.snake, try self.extractEnumFieldName(name, field.name)); + try self.writer.writeAll(" = "); + try self.writeIdentifierWithCase(.snake, try self.extractEnumFieldName(name, field.value.alias.name)); + try self.writer.writeAll(";"); + } + + try self.writer.writeAll("};\n"); + } + + fn renderAlias(self: *Self, name: []const u8, alias: reg.Alias) !void { + try self.writer.writeAll("const "); + try self.renderTypeName(name); + try self.writer.writeAll(" = "); + try self.renderTypeName(alias.name); + try self.writer.writeAll(";\n"); + } + + fn renderOpaque(self: *Self, name: []const u8) !void { + try self.writer.writeAll("const "); + try self.renderTypeName(name); + try self.writer.writeAll(" = @Type(.Opaque);\n"); + } + + fn renderTypedef(self: *Self, name: []const u8, type_info: reg.TypeInfo) !void { + try self.writer.writeAll("const "); + try self.renderTypeName(name); + try self.writer.writeAll(" = "); + try self.renderTypeInfo(type_info); + try self.writer.writeAll(";\n"); + } + }; +} + +pub fn render(writer: var, allocator: *Allocator, registry: *const reg.Registry) !void { + var renderer = Renderer(@TypeOf(writer)).init(writer, allocator, registry); + defer renderer.deinit(); + + try renderer.render(); +} diff --git a/generator/render/util.zig b/generator/render/util.zig new file mode 100644 index 0000000..3c5bc05 --- /dev/null +++ b/generator/render/util.zig @@ -0,0 +1,232 @@ +const std = @import("std"); +const reg = @import("../registry.zig"); +const mem = std.mem; +const Allocator = mem.Allocator; + +// Lifted from src-self-hosted/translate_c.zig +pub fn isValidZigIdentifier(name: []const u8) bool { + for (name) |c, i| { + switch (c) { + '_', 'a'...'z', 'A'...'Z' => {}, + '0' ... '9' => if (i == 0) return false, + else => return false + } + } + + return true; +} + +// Lifted from src-self-hosted/translate_c.zig +pub fn isZigReservedIdentifier(name: []const u8) bool { + if (name.len > 1 and (name[0] == 'u' or name[0] == 'i')) { + for (name[1..]) |c| { + switch (c) { + '0'...'9' => {}, + else => return false, + } + } + return true; + } + + const reserved_names = [_][]const u8 { + "void", "comptime_float", "comptime_int", "bool", "isize", + "usize", "f16", "f32", "f64", "f128", "c_longdouble", + "noreturn", "type", "anyerror", "c_short", "c_ushort", + "c_int", "c_uint", "c_long", "c_ulong", "c_longlong", "c_ulonglong" + }; + + for (reserved_names) |reserved| { + if (mem.eql(u8, reserved, name)) { + return true; + } + } + + return false; +} + +pub fn needZigEscape(name: []const u8) bool { + return !isValidZigIdentifier(name) + or isZigReservedIdentifier(name) + or std.zig.Token.getKeyword(name) != null; +} + +pub fn writeIdentifier(out: var, id: []const u8) !void { + if (needZigEscape(id)) { + try out.print("@\"{}\"", .{id}); + } else { + try out.writeAll(id); + } +} + +pub const CaseStyle = enum { + snake, + screaming_snake, + title, + camel, +}; + +pub fn trimVkNamespace(id: []const u8) []const u8 { + const prefixes = [_][]const u8{"VK_", "vk", "Vk", "PFN_vk"}; + for (prefixes) |prefix| { + if (mem.startsWith(u8, id, prefix)) { + return id[prefix.len..]; + } + } + + return id; +} + +pub fn getAuthorTag(id: []const u8, tags: []const reg.Tag) ?[]const u8 { + for (tags) |tag| { + if (mem.endsWith(u8, id, tag.name)) { + return tag.name; + } + } + + return null; +} + +pub const SegmentIterator = struct { + text: []const u8, + offset: usize, + + pub fn init(text: []const u8) SegmentIterator { + return .{ + .text = text, + .offset = 0, + }; + } + + fn nextBoundary(self: SegmentIterator) usize { + var i = self.offset + 1; + + while (true) { + if (i == self.text.len or self.text[i] == '_') { + return i; + } + + const prev_lower = std.ascii.isLower(self.text[i - 1]); + const next_lower = std.ascii.isLower(self.text[i]); + + if (prev_lower and !next_lower) { + return i; + } else if (i != self.offset + 1 and !prev_lower and next_lower) { + return i - 1; + } + + i += 1; + } + } + + pub fn next(self: *SegmentIterator) ?[]const u8 { + while (self.offset < self.text.len and self.text[self.offset] == '_') { + self.offset += 1; + } + + if (self.offset == self.text.len) { + return null; + } + + const end = self.nextBoundary(); + const word = self.text[self.offset .. end]; + self.offset = end; + return word; + } + + pub fn rest(self: SegmentIterator) []const u8 { + if (self.offset >= self.text.len) { + return &[_]u8{}; + } else { + return self.text[self.offset..]; + } + } +}; + +pub const IdRenderer = struct { + tags: []const reg.Tag, + text_cache: std.ArrayList(u8), + + pub fn init(allocator: *Allocator, tags: []const reg.Tag) IdRenderer { + return .{ + .tags = tags, + .text_cache = std.ArrayList(u8).init(allocator), + }; + } + + pub fn deinit(self: IdRenderer) void { + self.text_cache.deinit(); + } + + fn renderSnake(self: *IdRenderer, screaming: bool, id: []const u8, tag: ?[]const u8) !void { + var it = SegmentIterator.init(id); + var first = true; + const transform = if (screaming) std.ascii.toUpper else std.ascii.toLower; + + while (it.next()) |segment| { + if (first) { + first = false; + } else { + try self.text_cache.append('_'); + } + + for (segment) |c| { + try self.text_cache.append(transform(c)); + } + } + + if (tag) |name| { + try self.text_cache.append('_'); + + for (name) |c| { + try self.text_cache.append(transform(c)); + } + } + } + + fn renderCamel(self: *IdRenderer, title: bool, id: []const u8, tag: ?[]const u8) !void { + var it = SegmentIterator.init(id); + var lower_first = !title; + + while (it.next()) |segment| { + var i: usize = 0; + while (i < segment.len and std.ascii.isDigit(segment[i])) { + try self.text_cache.append(segment[i]); + i += 1; + } + + if (i == segment.len) { + continue; + } + + if (i == 0 and lower_first) { + try self.text_cache.append(std.ascii.toLower(segment[i])); + } else { + try self.text_cache.append(std.ascii.toUpper(segment[i])); + } + lower_first = false; + + for (segment[i + 1..]) |c| { + try self.text_cache.append(std.ascii.toLower(c)); + } + } + + if (tag) |name| { + try self.text_cache.appendSlice(name); + } + } + + pub fn render(self: *IdRenderer, out: var, case_style: CaseStyle, id: []const u8) !void { + const tag = getAuthorTag(id, self.tags); + const adjusted_id = if (tag) |name| id[0 .. id.len - name.len] else id; + self.text_cache.items.len = 0; + + switch (case_style) { + .snake => try self.renderSnake(false, adjusted_id, tag), + .screaming_snake => try self.renderSnake(true, adjusted_id, tag), + .title => try self.renderCamel(true, adjusted_id, tag), + .camel => try self.renderCamel(false, adjusted_id, tag), + } + + try writeIdentifier(out, self.text_cache.items); + } +};