Backlog/lib/sdl3/parser/src/codegen.zig

625 lines
24 KiB
Zig

const std = @import("std");
const Allocator = std.mem.Allocator;
const patterns = @import("patterns.zig");
const naming = @import("naming.zig");
const types = @import("types.zig");
const Declaration = patterns.Declaration;
const OpaqueType = patterns.OpaqueType;
const EnumDecl = patterns.EnumDecl;
const StructDecl = patterns.StructDecl;
const FlagDecl = patterns.FlagDecl;
pub const CodeGen = struct {
decls: []Declaration,
allocator: Allocator,
output: std.ArrayList(u8),
opaque_methods: std.StringHashMap(std.ArrayList(patterns.FunctionDecl)),
pub fn generate(allocator: Allocator, decls: []Declaration) ![]const u8 {
var gen = CodeGen{
.decls = decls,
.allocator = allocator,
.output = try std.ArrayList(u8).initCapacity(allocator, 4096),
.opaque_methods = std.StringHashMap(std.ArrayList(patterns.FunctionDecl)).init(allocator),
};
defer {
var it = gen.opaque_methods.valueIterator();
while (it.next()) |methods| {
methods.deinit(allocator);
}
gen.opaque_methods.deinit();
}
defer gen.output.deinit(allocator);
try gen.categorizeDeclarations();
try gen.writeHeader();
try gen.writeDeclarations();
return try gen.output.toOwnedSlice(allocator);
}
fn categorizeDeclarations(self: *CodeGen) !void {
// First, collect all opaque type names
var opaque_names = std.ArrayList([]const u8){};
defer opaque_names.deinit(self.allocator);
for (self.decls) |decl| {
if (decl == .opaque_type) {
const zig_name = naming.typeNameToZig(decl.opaque_type.name);
try opaque_names.append(self.allocator, zig_name);
// Initialize empty method list
try self.opaque_methods.put(zig_name, std.ArrayList(patterns.FunctionDecl){});
}
}
// Then, categorize functions
for (self.decls) |decl| {
if (decl == .function_decl) {
const func = decl.function_decl;
if (func.params.len > 0) {
// Check if first param is a pointer to an opaque type
const first_param_type = try types.convertType(func.params[0].type_name, self.allocator);
defer self.allocator.free(first_param_type);
// Check if it's ?*TypeName or *TypeName
for (opaque_names.items) |opaque_name| {
const opt_ptr = try std.fmt.allocPrint(self.allocator, "?*{s}", .{opaque_name});
defer self.allocator.free(opt_ptr);
const ptr = try std.fmt.allocPrint(self.allocator, "*{s}", .{opaque_name});
defer self.allocator.free(ptr);
if (std.mem.eql(u8, first_param_type, opt_ptr) or std.mem.eql(u8, first_param_type, ptr)) {
var methods = self.opaque_methods.getPtr(opaque_name).?;
try methods.append(self.allocator, func);
break;
}
}
}
}
}
}
fn writeHeader(self: *CodeGen) !void {
const header =
\\pub const c = @import("c.zig").c;
\\
\\
;
try self.output.appendSlice(self.allocator, header);
}
fn writeDeclarations(self: *CodeGen) !void {
// Generate each declaration
for (self.decls) |decl| {
switch (decl) {
.opaque_type => |opaque_decl| try self.writeOpaqueWithMethods(opaque_decl),
.typedef_decl => |typedef_decl| try self.writeTypedef(typedef_decl),
.enum_decl => |enum_decl| try self.writeEnum(enum_decl),
.struct_decl => |struct_decl| try self.writeStruct(struct_decl),
.flag_decl => |flag_decl| try self.writeFlags(flag_decl),
.function_decl => |func| {
// Only write standalone functions (not methods)
if (try self.isStandaloneFunction(func)) {
try self.writeFunction(func);
}
},
}
}
}
fn isStandaloneFunction(self: *CodeGen, func: patterns.FunctionDecl) !bool {
if (func.params.len == 0) return true;
const first_param_type = try types.convertType(func.params[0].type_name, self.allocator);
defer self.allocator.free(first_param_type);
var it = self.opaque_methods.keyIterator();
while (it.next()) |opaque_name| {
const opt_ptr = try std.fmt.allocPrint(self.allocator, "?*{s}", .{opaque_name.*});
defer self.allocator.free(opt_ptr);
const ptr = try std.fmt.allocPrint(self.allocator, "*{s}", .{opaque_name.*});
defer self.allocator.free(ptr);
if (std.mem.eql(u8, first_param_type, opt_ptr) or std.mem.eql(u8, first_param_type, ptr)) {
return false; // It's a method
}
}
return true; // It's standalone
}
fn writeOpaqueWithMethods(self: *CodeGen, opaque_type: OpaqueType) !void {
const zig_name = naming.typeNameToZig(opaque_type.name);
// Write doc comment if present
if (opaque_type.doc_comment) |doc| {
try self.writeDocComment(doc);
}
// Check if we have methods for this type
const methods = self.opaque_methods.get(zig_name);
if (methods) |method_list| {
if (method_list.items.len > 0) {
// pub const GPUDevice = opaque {
try self.output.writer(self.allocator).print("pub const {s} = opaque {{\n", .{zig_name});
// Write methods
for (method_list.items) |func| {
try self.writeFunctionAsMethod(func, zig_name);
}
try self.output.appendSlice(self.allocator, "};\n\n");
return;
}
}
// No methods, write as simple opaque
try self.output.writer(self.allocator).print("pub const {s} = opaque {{}};\n\n", .{zig_name});
}
fn writeTypedef(self: *CodeGen, typedef_decl: patterns.TypedefDecl) !void {
// Write doc comment if present
if (typedef_decl.doc_comment) |doc| {
try self.writeDocComment(doc);
}
const zig_name = naming.typeNameToZig(typedef_decl.name);
const zig_type = try types.convertType(typedef_decl.underlying_type, self.allocator);
defer self.allocator.free(zig_type);
try self.output.appendSlice(self.allocator, "pub const ");
try self.output.appendSlice(self.allocator, zig_name);
try self.output.appendSlice(self.allocator, " = ");
try self.output.appendSlice(self.allocator, zig_type);
try self.output.appendSlice(self.allocator, ";\n\n");
}
fn writeEnum(self: *CodeGen, enum_decl: EnumDecl) !void {
const zig_name = naming.typeNameToZig(enum_decl.name);
// Write doc comment if present
if (enum_decl.doc_comment) |doc| {
try self.writeDocComment(doc);
}
// pub const GPUPrimitiveType = enum(c_int) {
try self.output.writer(self.allocator).print("pub const {s} = enum(c_int) {{\n", .{zig_name});
// Detect common prefix
var value_names = try self.allocator.alloc([]const u8, enum_decl.values.len);
defer self.allocator.free(value_names);
for (enum_decl.values, 0..) |value, i| {
value_names[i] = value.name;
}
const prefix = try naming.detectCommonPrefix(value_names, self.allocator);
defer self.allocator.free(prefix);
// Write enum values
for (enum_decl.values) |value| {
const zig_value = try naming.enumValueToZig(value.name, prefix, self.allocator);
defer self.allocator.free(zig_value);
if (value.comment) |comment| {
try self.output.writer(self.allocator).print(" {s}, //{s}\n", .{ zig_value, comment });
} else {
try self.output.writer(self.allocator).print(" {s},\n", .{zig_value});
}
}
try self.output.appendSlice(self.allocator, "};\n\n");
}
fn writeStruct(self: *CodeGen, struct_decl: StructDecl) !void {
const zig_name = naming.typeNameToZig(struct_decl.name);
// Write doc comment if present
if (struct_decl.doc_comment) |doc| {
try self.writeDocComment(doc);
}
// pub const GPUViewport = extern struct {
try self.output.writer(self.allocator).print("pub const {s} = extern struct {{\n", .{zig_name});
// Write fields
for (struct_decl.fields) |field| {
const zig_type = try types.convertType(field.type_name, self.allocator);
defer self.allocator.free(zig_type);
if (field.comment) |comment| {
try self.output.writer(self.allocator).print(" {s}: {s}, // {s}\n", .{
field.name,
zig_type,
comment,
});
} else {
try self.output.writer(self.allocator).print(" {s}: {s},\n", .{ field.name, zig_type });
}
}
try self.output.appendSlice(self.allocator, "};\n\n");
}
fn writeFlags(self: *CodeGen, flag_decl: FlagDecl) !void {
const zig_name = naming.typeNameToZig(flag_decl.name);
// Write doc comment if present
if (flag_decl.doc_comment) |doc| {
try self.writeDocComment(doc);
}
// Determine underlying type size (u8, u16, u32, u64)
const underlying_type = if (std.mem.eql(u8, flag_decl.underlying_type, "Uint8"))
"u8"
else if (std.mem.eql(u8, flag_decl.underlying_type, "Uint16"))
"u16"
else if (std.mem.eql(u8, flag_decl.underlying_type, "Uint64"))
"u64"
else
"u32";
const type_bits: u32 = if (std.mem.eql(u8, underlying_type, "u8"))
8
else if (std.mem.eql(u8, underlying_type, "u16"))
16
else if (std.mem.eql(u8, underlying_type, "u64"))
64
else
32;
// pub const GPUTextureUsageFlags = packed struct(u32) {
try self.output.writer(self.allocator).print("pub const {s} = packed struct({s}) {{\n", .{
zig_name,
underlying_type,
});
// Detect common prefix
var flag_names = try self.allocator.alloc([]const u8, flag_decl.flags.len);
defer self.allocator.free(flag_names);
for (flag_decl.flags, 0..) |flag, i| {
flag_names[i] = flag.name;
}
const prefix = try naming.detectCommonPrefix(flag_names, self.allocator);
defer self.allocator.free(prefix);
// Track which bits are used
var used_bits = std.bit_set.IntegerBitSet(64).initEmpty();
// Write flag fields
for (flag_decl.flags) |flag| {
const zig_flag = try naming.flagNameToZig(flag.name, prefix, self.allocator);
defer self.allocator.free(zig_flag);
// Parse bit position from value like "(1u << 0)"
const bit_pos = try self.parseBitPosition(flag.value);
used_bits.set(bit_pos);
if (flag.comment) |comment| {
try self.output.writer(self.allocator).print(" {s}: bool = false, // {s}\n", .{
zig_flag,
comment,
});
} else {
try self.output.writer(self.allocator).print(" {s}: bool = false,\n", .{zig_flag});
}
}
// Calculate padding
const used_count = used_bits.count();
const padding_bits = type_bits - used_count - 1; // -1 for reserved bit
if (padding_bits > 0) {
try self.output.writer(self.allocator).print(" pad0: u{d} = 0,\n", .{padding_bits});
}
// Always add a reserved bit at the end
try self.output.appendSlice(self.allocator, " rsvd: bool = false,\n");
try self.output.appendSlice(self.allocator, "};\n\n");
}
fn writeFunctionAsMethod(self: *CodeGen, func: patterns.FunctionDecl, owner_type: []const u8) !void {
const zig_name = try naming.functionNameToZig(func.name, self.allocator);
defer self.allocator.free(zig_name);
// Write doc comment if present
if (func.doc_comment) |doc| {
try self.writeDocComment(doc);
}
// Convert return type
const zig_return_type = try types.convertType(func.return_type, self.allocator);
defer self.allocator.free(zig_return_type);
// pub inline fn createGPUDevice(
try self.output.writer(self.allocator).print(" pub inline fn {s}(", .{zig_name});
// Write parameters - first param is renamed to lowercase type name
for (func.params, 0..) |param, i| {
const zig_type = try types.convertType(param.type_name, self.allocator);
defer self.allocator.free(zig_type);
if (i > 0) {
try self.output.appendSlice(self.allocator, ", ");
}
if (i == 0) {
// First parameter: use lowercase owner type name and non-nullable pointer
const lower_name = try std.ascii.allocLowerString(self.allocator, owner_type);
defer self.allocator.free(lower_name);
// Remove ? from type if present
const non_nullable = if (std.mem.startsWith(u8, zig_type, "?*"))
zig_type[1..]
else
zig_type;
try self.output.writer(self.allocator).print("{s}: {s}", .{ lower_name, non_nullable });
} else if (param.name.len > 0) {
try self.output.writer(self.allocator).print("{s}: {s}", .{ param.name, zig_type });
} else {
try self.output.writer(self.allocator).print("{s}", .{zig_type});
}
}
// ) *GPUDevice {
// Add trailing comma for functions with more than 3 parameters (triggers multi-line formatting)
if (func.params.len > 3) {
try self.output.writer(self.allocator).print(",) {s} {{\n", .{zig_return_type});
} else {
try self.output.writer(self.allocator).print(") {s} {{\n", .{zig_return_type});
}
// Function body - call C API with appropriate casts
try self.output.appendSlice(self.allocator, " return ");
// Determine if we need a cast
const needs_cast = !std.mem.eql(u8, zig_return_type, "void");
const return_cast = if (needs_cast) types.getCastType(zig_return_type) else .none;
if (return_cast != .none) {
const cast_str = castTypeToString(return_cast);
try self.output.writer(self.allocator).print("{s}(", .{cast_str});
}
// c.SDL_FunctionName(
try self.output.writer(self.allocator).print("c.{s}(", .{func.name});
// Pass parameters with casts
for (func.params, 0..) |param, i| {
if (i > 0) {
try self.output.appendSlice(self.allocator, ", ");
}
if (param.name.len > 0 or i == 0) {
const param_name = if (i == 0) blk: {
const lower = try std.ascii.allocLowerString(self.allocator, owner_type);
defer self.allocator.free(lower);
break :blk try self.allocator.dupe(u8, lower);
} else try self.allocator.dupe(u8, param.name);
defer self.allocator.free(param_name);
const zig_param_type = try types.convertType(param.type_name, self.allocator);
defer self.allocator.free(zig_param_type);
const param_cast = types.getCastType(zig_param_type);
if (param_cast == .none) {
try self.output.writer(self.allocator).print("{s}", .{param_name});
} else {
const cast_str = castTypeToString(param_cast);
try self.output.writer(self.allocator).print("{s}({s})", .{ cast_str, param_name });
}
}
}
// Close the call
if (return_cast != .none) {
try self.output.appendSlice(self.allocator, "));\n");
} else {
try self.output.appendSlice(self.allocator, ");\n");
}
try self.output.appendSlice(self.allocator, " }\n\n");
}
fn writeFunction(self: *CodeGen, func: patterns.FunctionDecl) !void {
const zig_name = try naming.functionNameToZig(func.name, self.allocator);
defer self.allocator.free(zig_name);
// Write doc comment if present
if (func.doc_comment) |doc| {
try self.writeDocComment(doc);
}
// Convert return type
const zig_return_type = try types.convertType(func.return_type, self.allocator);
defer self.allocator.free(zig_return_type);
// pub inline fn createGPUDevice(
try self.output.writer(self.allocator).print("pub inline fn {s}(", .{zig_name});
// Write parameters
for (func.params, 0..) |param, i| {
const zig_type = try types.convertType(param.type_name, self.allocator);
defer self.allocator.free(zig_type);
if (i > 0) {
try self.output.appendSlice(self.allocator, ", ");
}
if (param.name.len > 0) {
try self.output.writer(self.allocator).print("{s}: {s}", .{ param.name, zig_type });
} else {
// Parameter has no name (like void or unnamed param)
try self.output.writer(self.allocator).print("{s}", .{zig_type});
}
}
// ) *GPUDevice {
// Add trailing comma for functions with more than 3 parameters (triggers multi-line formatting)
if (func.params.len > 3) {
try self.output.writer(self.allocator).print(",) {s} {{\n", .{zig_return_type});
} else {
try self.output.writer(self.allocator).print(") {s} {{\n", .{zig_return_type});
}
// Function body - call C API with appropriate casts
try self.output.appendSlice(self.allocator, " return ");
// Determine if we need a cast
const needs_cast = !std.mem.eql(u8, zig_return_type, "void");
const return_cast = if (needs_cast) types.getCastType(zig_return_type) else .none;
if (return_cast != .none) {
const cast_str = castTypeToString(return_cast);
try self.output.writer(self.allocator).print("{s}(", .{cast_str});
}
// c.SDL_FunctionName(
try self.output.writer(self.allocator).print("c.{s}(", .{func.name});
// Pass parameters with casts
for (func.params, 0..) |param, i| {
if (i > 0) {
try self.output.appendSlice(self.allocator, ", ");
}
if (param.name.len > 0) {
const zig_param_type = try types.convertType(param.type_name, self.allocator);
defer self.allocator.free(zig_param_type);
const param_cast = types.getCastType(zig_param_type);
if (param_cast == .none) {
try self.output.writer(self.allocator).print("{s}", .{param.name});
} else {
const cast_str = castTypeToString(param_cast);
try self.output.writer(self.allocator).print("{s}({s})", .{ cast_str, param.name });
}
}
}
// Close the call
if (return_cast != .none) {
try self.output.appendSlice(self.allocator, "));\n");
} else {
try self.output.appendSlice(self.allocator, ");\n");
}
try self.output.appendSlice(self.allocator, "}\n\n");
}
fn castTypeToString(cast_type: types.CastType) []const u8 {
return switch (cast_type) {
.none => "none",
.ptr_cast => "@ptrCast",
.bit_cast => "@bitCast",
.int_from_enum => "@intFromEnum",
.enum_from_int => "@enumFromInt",
};
}
fn writeDocComment(self: *CodeGen, comment: []const u8) !void {
// For now, just skip doc comments
// TODO: Parse and format doc comments properly
_ = self;
_ = comment;
}
fn parseBitPosition(self: *CodeGen, value: []const u8) !u6 {
_ = self;
// Parse expressions like "(1u << 0)" or "0x01" or "SDL_UINT64_C(0x...)"
var trimmed = std.mem.trim(u8, value, " \t()");
// Handle SDL_UINT64_C(0x...) pattern
if (std.mem.startsWith(u8, trimmed, "SDL_UINT64_C(")) {
const inner_start = "SDL_UINT64_C(".len;
trimmed = std.mem.trim(u8, trimmed[inner_start..], " \t)");
}
// Look for bit shift pattern: "1u << N"
if (std.mem.indexOf(u8, trimmed, "<<")) |shift_pos| {
const after_shift = std.mem.trim(u8, trimmed[shift_pos + 2 ..], " \t)");
const bit = try std.fmt.parseInt(u6, after_shift, 10);
return bit;
}
// Hex value like "0x01" or "0x0000000000000001"
if (std.mem.startsWith(u8, trimmed, "0x")) {
const hex_str = trimmed[2..];
const val = try std.fmt.parseInt(u64, hex_str, 16);
// Find the bit position (count trailing zeros)
var bit: u6 = 0;
while (bit < 64) : (bit += 1) {
if (val == (@as(u64, 1) << @as(u6, bit))) return bit;
}
}
return error.InvalidBitPosition;
}
};
test "generate opaque type" {
const opaque_type = OpaqueType{
.name = "SDL_GPUDevice",
.doc_comment = null,
};
var decls = [_]Declaration{.{ .opaque_type = opaque_type }};
const output = try CodeGen.generate(std.testing.allocator, decls[0..]);
defer std.testing.allocator.free(output);
const expected =
\\pub const c = @import("c.zig").c;
\\
\\pub const GPUDevice = opaque {};
\\
\\
;
try std.testing.expectEqualStrings(expected, output);
}
test "generate enum" {
var values = [_]patterns.EnumValue{
.{
.name = "SDL_GPU_PRIMITIVETYPE_TRIANGLELIST",
.value = null,
.comment = " A series of triangles",
},
.{
.name = "SDL_GPU_PRIMITIVETYPE_LINELIST",
.value = null,
.comment = " A series of lines",
},
};
const enum_decl = EnumDecl{
.name = "SDL_GPUPrimitiveType",
.values = values[0..],
.doc_comment = null,
};
var decls = [_]Declaration{.{ .enum_decl = enum_decl }};
const output = try CodeGen.generate(std.testing.allocator, decls[0..]);
defer std.testing.allocator.free(output);
// Verify it contains the expected elements
try std.testing.expect(std.mem.indexOf(u8, output, "pub const GPUPrimitiveType = enum(c_int)") != null);
try std.testing.expect(std.mem.indexOf(u8, output, "trianglelist") != null);
try std.testing.expect(std.mem.indexOf(u8, output, "linelist") != null);
}
test "parse bit position" {
var gen = CodeGen{
.decls = &[_]Declaration{},
.allocator = std.testing.allocator,
.output = try std.ArrayList(u8).initCapacity(std.testing.allocator, 1),
};
defer gen.output.deinit(std.testing.allocator);
try std.testing.expectEqual(@as(u6, 0), try gen.parseBitPosition("(1u << 0)"));
try std.testing.expectEqual(@as(u6, 5), try gen.parseBitPosition("1u << 5"));
try std.testing.expectEqual(@as(u6, 0), try gen.parseBitPosition("0x01"));
try std.testing.expectEqual(@as(u6, 3), try gen.parseBitPosition("0x08"));
}