1585 lines
61 KiB
Zig
1585 lines
61 KiB
Zig
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
fn fixupZigName(name: []const u8) []u8 {
|
|
const allocator = std.heap.smp_allocator;
|
|
if (std.mem.eql(u8, name, "type")) {
|
|
return allocator.dupe(u8, "_type") catch unreachable;
|
|
}
|
|
|
|
return allocator.dupe(u8, name) catch unreachable;
|
|
}
|
|
|
|
// Simple data structures to hold extracted declarations
|
|
pub const Declaration = union(enum) {
|
|
opaque_type: OpaqueType,
|
|
enum_decl: EnumDecl,
|
|
struct_decl: StructDecl,
|
|
union_decl: UnionDecl,
|
|
flag_decl: FlagDecl,
|
|
function_decl: FunctionDecl,
|
|
typedef_decl: TypedefDecl,
|
|
function_pointer_decl: FunctionPointerDecl,
|
|
c_type_alias: CTypeAlias,
|
|
};
|
|
|
|
pub const OpaqueType = struct {
|
|
name: []const u8, // SDL_GPUDevice
|
|
doc_comment: ?[]const u8,
|
|
};
|
|
|
|
pub const EnumDecl = struct {
|
|
name: []const u8, // SDL_GPUPrimitiveType
|
|
values: []EnumValue,
|
|
doc_comment: ?[]const u8,
|
|
};
|
|
|
|
pub const EnumValue = struct {
|
|
name: []const u8, // SDL_GPU_PRIMITIVETYPE_TRIANGLELIST
|
|
value: ?[]const u8, // Optional explicit value
|
|
comment: ?[]const u8, // Inline comment
|
|
};
|
|
|
|
pub const StructDecl = struct {
|
|
name: []const u8, // SDL_GPUViewport
|
|
fields: []FieldDecl,
|
|
doc_comment: ?[]const u8,
|
|
has_unions: bool = false, // If true, codegen should emit as opaque (C unions can't be represented in other languages)
|
|
};
|
|
|
|
pub const UnionDecl = struct {
|
|
name: []const u8, // SDL_Event
|
|
fields: []FieldDecl,
|
|
doc_comment: ?[]const u8,
|
|
};
|
|
|
|
pub const FieldDecl = struct {
|
|
name: []const u8, // x
|
|
type_name: []const u8, // float
|
|
comment: ?[]const u8,
|
|
};
|
|
|
|
pub const FlagDecl = struct {
|
|
name: []const u8, // SDL_GPUTextureUsageFlags
|
|
underlying_type: []const u8, // Uint32
|
|
flags: []FlagValue,
|
|
doc_comment: ?[]const u8,
|
|
};
|
|
|
|
pub const FlagValue = struct {
|
|
name: []const u8, // SDL_GPU_TEXTUREUSAGE_SAMPLER
|
|
value: []const u8, // (1u << 0)
|
|
comment: ?[]const u8,
|
|
};
|
|
|
|
pub const TypedefDecl = struct {
|
|
name: []const u8, // SDL_PropertiesID
|
|
underlying_type: []const u8, // Uint32
|
|
doc_comment: ?[]const u8,
|
|
};
|
|
|
|
pub const FunctionPointerDecl = struct {
|
|
name: []const u8, // SDL_TimerCallback
|
|
return_type: []const u8, // Uint32
|
|
params: []ParamDecl,
|
|
doc_comment: ?[]const u8,
|
|
};
|
|
|
|
/// C type alias - for function pointer typedefs that should alias to C type directly
|
|
/// Output: pub const Name = c.SDL_Name;
|
|
pub const CTypeAlias = struct {
|
|
name: []const u8, // SDL_HitTest
|
|
doc_comment: ?[]const u8,
|
|
};
|
|
|
|
pub const FunctionDecl = struct {
|
|
name: []const u8, // SDL_CreateGPUDevice
|
|
return_type: []const u8, // SDL_GPUDevice *
|
|
params: []ParamDecl,
|
|
doc_comment: ?[]const u8,
|
|
};
|
|
|
|
pub const ParamDecl = struct {
|
|
name: []const u8, // format_flags
|
|
type_name: []const u8, // SDL_GPUShaderFormat
|
|
};
|
|
|
|
pub const Scanner = struct {
|
|
source: []const u8,
|
|
pos: usize,
|
|
allocator: Allocator,
|
|
pending_doc_comment: ?[]const u8,
|
|
|
|
pub fn init(allocator: Allocator, source: []const u8) Scanner {
|
|
return .{
|
|
.source = source,
|
|
.pos = 0,
|
|
.allocator = allocator,
|
|
.pending_doc_comment = null,
|
|
};
|
|
}
|
|
|
|
pub fn scan(self: *Scanner) ![]Declaration {
|
|
var decls = try std.ArrayList(Declaration).initCapacity(self.allocator, 100);
|
|
|
|
while (!self.isAtEnd()) {
|
|
// Try to extract doc comment
|
|
if (self.peekDocComment()) |comment| {
|
|
self.pending_doc_comment = comment;
|
|
}
|
|
|
|
// Try each pattern - order matters!
|
|
// Try opaque first (typedef struct SDL_X SDL_X;)
|
|
if (try self.scanOpaque()) |opaque_decl| {
|
|
try decls.append(self.allocator, .{ .opaque_type = opaque_decl });
|
|
} else if (try self.scanEnum()) |enum_decl| {
|
|
try decls.append(self.allocator, .{ .enum_decl = enum_decl });
|
|
} else if (try self.scanStruct()) |struct_decl| {
|
|
try decls.append(self.allocator, .{ .struct_decl = struct_decl });
|
|
} else if (try self.scanUnion()) |union_decl| {
|
|
try decls.append(self.allocator, .{ .union_decl = union_decl });
|
|
} else if (try self.scanFlagTypedef()) |flag_decl| {
|
|
// Flag typedef must come before simple typedef
|
|
try decls.append(self.allocator, .{ .flag_decl = flag_decl });
|
|
} else if (try self.scanFunctionPointer()) |c_alias| {
|
|
// Function pointer typedef -> C type alias (must come before simple typedef)
|
|
try decls.append(self.allocator, .{ .c_type_alias = c_alias });
|
|
} else if (try self.scanTypedef()) |typedef_decl| {
|
|
// Simple typedef comes after flag typedef
|
|
try decls.append(self.allocator, .{ .typedef_decl = typedef_decl });
|
|
} else if (try self.scanFunction()) |func| {
|
|
try decls.append(self.allocator, .{ .function_decl = func });
|
|
} else {
|
|
// Skip this line - but first free any pending doc comment
|
|
if (self.pending_doc_comment) |comment| {
|
|
self.allocator.free(comment);
|
|
self.pending_doc_comment = null;
|
|
}
|
|
self.skipLine();
|
|
}
|
|
}
|
|
|
|
return try decls.toOwnedSlice(self.allocator);
|
|
}
|
|
|
|
// Pattern: typedef struct SDL_Foo SDL_Foo;
|
|
fn scanOpaque(self: *Scanner) !?OpaqueType {
|
|
const start = self.pos;
|
|
|
|
// Read the whole line first
|
|
const line = try self.readLine();
|
|
defer self.allocator.free(line);
|
|
|
|
// Check if it matches the pattern
|
|
if (!std.mem.startsWith(u8, line, "typedef struct ")) {
|
|
self.pos = start;
|
|
return null;
|
|
}
|
|
|
|
// Extract name from "typedef struct SDL_Foo SDL_Foo;"
|
|
var iter = std.mem.tokenizeScalar(u8, line, ' ');
|
|
_ = iter.next(); // typedef
|
|
_ = iter.next(); // struct
|
|
const name1 = iter.next() orelse {
|
|
self.pos = start;
|
|
return null;
|
|
};
|
|
const name2 = iter.next() orelse {
|
|
self.pos = start;
|
|
return null;
|
|
};
|
|
|
|
// Check they match and end with semicolon
|
|
// Or accept mismatched names as long as we have a semicolon (e.g., typedef struct tagMSG MSG;)
|
|
// But reject pointer typedefs (e.g., typedef struct X *Y;) - those should be handled by scanTypedef
|
|
const name2_clean = std.mem.trimRight(u8, name2, ";");
|
|
|
|
// Check if it's a pointer typedef - if either name starts with *, reject it
|
|
if (std.mem.startsWith(u8, name1, "*") or std.mem.startsWith(u8, name2_clean, "*")) {
|
|
self.pos = start;
|
|
return null;
|
|
}
|
|
|
|
const use_name = if (std.mem.eql(u8, name1, name2_clean))
|
|
name1 // Names match, use either
|
|
else if (std.mem.endsWith(u8, name2, ";"))
|
|
name2_clean // Names don't match but it's a valid forward declaration, use second name
|
|
else {
|
|
// Not a valid opaque typedef
|
|
self.pos = start;
|
|
return null;
|
|
};
|
|
|
|
// This is an opaque type (not a struct definition with braces)
|
|
// Make sure it doesn't have braces
|
|
if (std.mem.indexOfScalar(u8, line, '{')) |_| {
|
|
self.pos = start;
|
|
return null;
|
|
}
|
|
|
|
const name = try self.allocator.dupe(u8, use_name);
|
|
const doc = self.consumePendingDocComment();
|
|
|
|
return OpaqueType{
|
|
.name = name,
|
|
.doc_comment = doc,
|
|
};
|
|
}
|
|
|
|
// Pattern: typedef RetType (SDLCALL *FuncName)(Param1Type param1, ...);
|
|
// or: typedef RetType (*FuncName)(Param1Type param1, ...);
|
|
// All function pointer typedefs become C type aliases: pub const Name = c.SDL_Name;
|
|
fn scanFunctionPointer(self: *Scanner) !?CTypeAlias {
|
|
const start = self.pos;
|
|
|
|
const line = try self.readLine();
|
|
defer self.allocator.free(line);
|
|
|
|
// Must start with typedef
|
|
if (!std.mem.startsWith(u8, line, "typedef ")) {
|
|
self.pos = start;
|
|
return null;
|
|
}
|
|
|
|
// Must contain * pattern with SDL prefix (function pointer typedef)
|
|
// Pattern: typedef RetType (SDLCALL *SDL_Name)(Params);
|
|
// or: typedef RetType (*SDL_Name)(Params);
|
|
const has_sdl_ptr = std.mem.indexOf(u8, line, " *SDL_") != null or
|
|
std.mem.indexOf(u8, line, "(*SDL_") != null;
|
|
if (!has_sdl_ptr) {
|
|
self.pos = start;
|
|
return null;
|
|
}
|
|
|
|
// Must have two sets of parentheses (function pointer pattern)
|
|
const first_close = std.mem.indexOfScalar(u8, line, ')') orelse {
|
|
self.pos = start;
|
|
return null;
|
|
};
|
|
// Check for second set of parens after the first close
|
|
if (std.mem.indexOfScalarPos(u8, line, first_close + 1, '(') == null) {
|
|
self.pos = start;
|
|
return null;
|
|
}
|
|
|
|
// Extract function name from between *SDL_ and )
|
|
// Find *SDL_ marker
|
|
const star_sdl = std.mem.indexOf(u8, line, "*SDL_") orelse {
|
|
self.pos = start;
|
|
return null;
|
|
};
|
|
|
|
// Name starts after * and ends at )
|
|
const name_start = star_sdl + 1; // Skip *
|
|
const name_end_search = line[name_start..];
|
|
const name_end_offset = std.mem.indexOfScalar(u8, name_end_search, ')') orelse {
|
|
self.pos = start;
|
|
return null;
|
|
};
|
|
const func_name = std.mem.trim(u8, name_end_search[0..name_end_offset], " \t");
|
|
|
|
const doc = self.consumePendingDocComment();
|
|
|
|
return CTypeAlias{
|
|
.name = try self.allocator.dupe(u8, func_name),
|
|
.doc_comment = doc,
|
|
};
|
|
}
|
|
|
|
// Pattern: typedef Type SDL_Name;
|
|
fn scanTypedef(self: *Scanner) !?TypedefDecl {
|
|
const start = self.pos;
|
|
|
|
const line = try self.readLine();
|
|
defer self.allocator.free(line);
|
|
|
|
// Check if it matches: typedef <type> <name>;
|
|
if (!std.mem.startsWith(u8, line, "typedef ")) {
|
|
self.pos = start;
|
|
return null;
|
|
}
|
|
|
|
// Skip lines with braces (those are struct/enum typedefs, handled elsewhere)
|
|
if (std.mem.indexOf(u8, line, "{") != null) {
|
|
self.pos = start;
|
|
return null;
|
|
}
|
|
|
|
// Skip lines with "struct" or "enum" keywords UNLESS it's a pointer typedef like:
|
|
// typedef struct X *Y;
|
|
const has_struct_or_enum = std.mem.indexOf(u8, line, "struct ") != null or std.mem.indexOf(u8, line, "enum ") != null;
|
|
if (has_struct_or_enum) {
|
|
// Check if it's a pointer typedef: should have * before the final name
|
|
const trimmed_check = std.mem.trim(u8, line, " \t\r\n;");
|
|
const has_pointer = std.mem.indexOf(u8, trimmed_check, " *") != null;
|
|
if (!has_pointer) {
|
|
// Not a pointer typedef, skip it
|
|
self.pos = start;
|
|
return null;
|
|
}
|
|
// It's a pointer typedef like "typedef struct X *Y", continue parsing
|
|
}
|
|
|
|
// Skip function pointer typedefs (contain parentheses)
|
|
if (std.mem.indexOf(u8, line, "(") != null) {
|
|
self.pos = start;
|
|
return null;
|
|
}
|
|
|
|
// Parse: typedef Type Name; or typedef struct X *Name;
|
|
const trimmed = std.mem.trim(u8, line, " \t\r\n");
|
|
const no_semi = std.mem.trimRight(u8, trimmed, ";");
|
|
|
|
// Find the last token as the name
|
|
var tokens = std.mem.tokenizeScalar(u8, no_semi, ' ');
|
|
_ = tokens.next(); // Skip "typedef"
|
|
|
|
// Collect all remaining tokens
|
|
var token_list = std.ArrayList([]const u8).initCapacity(self.allocator, 4) catch {
|
|
self.pos = start;
|
|
return null;
|
|
};
|
|
defer token_list.deinit(self.allocator);
|
|
while (tokens.next()) |token| {
|
|
try token_list.append(self.allocator, token);
|
|
}
|
|
|
|
if (token_list.items.len < 2) {
|
|
self.pos = start;
|
|
return null;
|
|
}
|
|
|
|
// Last token is the name (may have * prefix for pointer typedefs)
|
|
var name_raw = token_list.items[token_list.items.len - 1];
|
|
// Strip leading * if present and track it
|
|
const has_pointer_prefix = std.mem.startsWith(u8, name_raw, "*");
|
|
const name = if (has_pointer_prefix)
|
|
name_raw[1..]
|
|
else
|
|
name_raw;
|
|
|
|
// Everything before the name is the underlying type
|
|
// For "struct XTaskQueueObject *XTaskQueueHandle", we want "struct XTaskQueueObject *"
|
|
var type_buf = std.ArrayList(u8).initCapacity(self.allocator, 64) catch {
|
|
self.pos = start;
|
|
return null;
|
|
};
|
|
defer type_buf.deinit(self.allocator);
|
|
for (token_list.items[0 .. token_list.items.len - 1], 0..) |token, i| {
|
|
if (i > 0) try type_buf.append(self.allocator, ' ');
|
|
try type_buf.appendSlice(self.allocator, token);
|
|
}
|
|
// Add the * if it was part of the name token
|
|
if (has_pointer_prefix) {
|
|
try type_buf.append(self.allocator, ' ');
|
|
try type_buf.append(self.allocator, '*');
|
|
}
|
|
const underlying_type = try type_buf.toOwnedSlice(self.allocator);
|
|
|
|
// Make sure it's an SDL type or one of the known Windows types
|
|
if (!std.mem.startsWith(u8, name, "SDL_") and
|
|
!std.mem.eql(u8, name, "XTaskQueueHandle") and
|
|
!std.mem.eql(u8, name, "XUserHandle"))
|
|
{
|
|
self.allocator.free(underlying_type);
|
|
self.pos = start;
|
|
return null;
|
|
}
|
|
|
|
return TypedefDecl{
|
|
.name = try self.allocator.dupe(u8, name),
|
|
.underlying_type = try self.allocator.dupe(u8, underlying_type),
|
|
.doc_comment = self.consumePendingDocComment(),
|
|
};
|
|
}
|
|
|
|
fn countChar(need: u8, haystack: []const u8) u32 {
|
|
var i: u32 = 0;
|
|
for (haystack) |h| {
|
|
if (h == need) {
|
|
i += 1;
|
|
}
|
|
}
|
|
return i;
|
|
}
|
|
|
|
// Pattern: typedef enum SDL_Foo { ... } SDL_Foo;
|
|
fn scanEnum(self: *Scanner) !?EnumDecl {
|
|
const start = self.pos;
|
|
|
|
if (!self.matchPrefix("typedef enum ")) {
|
|
return null;
|
|
}
|
|
|
|
// Find the opening brace and extract the name before it
|
|
// But stop if we hit a semicolon (indicates forward declaration)
|
|
// Allow newlines/whitespace before the brace
|
|
const name_start = self.pos;
|
|
var found_semicolon = false;
|
|
while (self.pos < self.source.len and self.source[self.pos] != '{') {
|
|
if (self.source[self.pos] == ';') {
|
|
found_semicolon = true;
|
|
break;
|
|
}
|
|
self.pos += 1;
|
|
}
|
|
|
|
if (self.pos >= self.source.len or found_semicolon or self.source[self.pos] != '{') {
|
|
self.pos = start;
|
|
return null;
|
|
}
|
|
|
|
// Extract name from between "typedef enum " and "{"
|
|
const name_slice = std.mem.trim(u8, self.source[name_start..self.pos], " \t\n\r");
|
|
var iter = std.mem.tokenizeScalar(u8, name_slice, ' ');
|
|
const name = iter.next() orelse {
|
|
self.pos = start;
|
|
return null;
|
|
};
|
|
|
|
// Now we're at the opening brace, read the braced block
|
|
const body = try self.readBracedBlock();
|
|
defer self.allocator.free(body);
|
|
|
|
// Parse enum values from body
|
|
var values = try std.ArrayList(EnumValue).initCapacity(self.allocator, 20);
|
|
var seen_names = std.StringHashMap(void).init(self.allocator);
|
|
defer {
|
|
var it = seen_names.keyIterator();
|
|
while (it.next()) |key| {
|
|
self.allocator.free(key.*);
|
|
}
|
|
seen_names.deinit();
|
|
}
|
|
|
|
var lines = std.mem.splitScalar(u8, body, '\n');
|
|
var in_multiline_comment = false;
|
|
|
|
while (lines.next()) |line| {
|
|
var trimmed = std.mem.trim(u8, line, " \t\r");
|
|
if (trimmed.len == 0) continue;
|
|
|
|
if (in_multiline_comment) {
|
|
if (std.mem.indexOf(u8, trimmed, "*/")) |x| {
|
|
in_multiline_comment = false;
|
|
if (trimmed.len == 2)
|
|
continue;
|
|
trimmed = trimmed[x + 2 ..];
|
|
}
|
|
}
|
|
|
|
// Track multi-line comments (both /** and /* styles)
|
|
if (std.mem.indexOf(u8, trimmed, "/*")) |_| {
|
|
in_multiline_comment = true;
|
|
}
|
|
|
|
if (in_multiline_comment) {
|
|
if (std.mem.indexOf(u8, trimmed, "*/")) |_| {
|
|
in_multiline_comment = false;
|
|
}
|
|
}
|
|
if (std.mem.indexOf(u8, trimmed, "/*")) |x| {
|
|
if (x == 0) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// special case for those weirder multiline comments
|
|
if (std.mem.indexOf(u8, trimmed, "/*") == null and std.mem.indexOf(u8, trimmed, "*/") == null) {
|
|
if (countChar(' ', trimmed) > 0) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Skip various comment/bracket/preprocessor lines
|
|
if (std.mem.startsWith(u8, trimmed, "//")) continue;
|
|
if (std.mem.startsWith(u8, trimmed, "*")) continue; // Lines inside comments
|
|
if (std.mem.startsWith(u8, trimmed, "#")) continue; // Preprocessor directives
|
|
if (std.mem.startsWith(u8, trimmed, "{")) continue;
|
|
if (std.mem.startsWith(u8, trimmed, "}")) continue;
|
|
|
|
if (try self.parseEnumValue(trimmed)) |value| {
|
|
// Check for duplicate names (from #if/#else branches)
|
|
if (!seen_names.contains(value.name)) {
|
|
const name_copy = try self.allocator.dupe(u8, value.name);
|
|
try seen_names.put(name_copy, {});
|
|
try values.append(self.allocator, value);
|
|
} else {
|
|
// Skip duplicate, free the value
|
|
self.allocator.free(value.name);
|
|
if (value.value) |v| self.allocator.free(v);
|
|
if (value.comment) |c| self.allocator.free(c);
|
|
}
|
|
}
|
|
}
|
|
|
|
const doc = self.consumePendingDocComment();
|
|
|
|
return EnumDecl{
|
|
.name = try self.allocator.dupe(u8, name),
|
|
.values = try values.toOwnedSlice(self.allocator),
|
|
.doc_comment = doc,
|
|
};
|
|
}
|
|
|
|
fn parseEnumValue(self: *Scanner, line: []const u8) !?EnumValue {
|
|
// Format: SDL_GPU_PRIMITIVETYPE_TRIANGLELIST, /**< comment */
|
|
// or: SDL_GPU_PRIMITIVETYPE_TRIANGLELIST = 5, /**< comment */
|
|
// or: SDL_GPU_PRIMITIVETYPE_POINTLIST /**< comment */ (last value, no comma)
|
|
|
|
var parts = std.mem.splitScalar(u8, line, ',');
|
|
const first = std.mem.trim(u8, parts.next() orelse return null, " \t");
|
|
if (first.len == 0) return null;
|
|
|
|
// Extract inline comment if present (check both before and after comma)
|
|
var comment: ?[]const u8 = null;
|
|
const comment_search = if (parts.rest().len > 0) parts.rest() else first;
|
|
if (std.mem.indexOf(u8, comment_search, "/**<")) |start| {
|
|
if (std.mem.indexOf(u8, comment_search[start..], "*/")) |end_offset| {
|
|
const comment_text = comment_search[start + 4 .. start + end_offset];
|
|
comment = try self.allocator.dupe(u8, std.mem.trim(u8, comment_text, " \t"));
|
|
}
|
|
}
|
|
|
|
// Extract name and optional value (strip comment if it was in first part)
|
|
var name_part = first;
|
|
if (std.mem.indexOf(u8, first, "/**<")) |comment_pos| {
|
|
name_part = std.mem.trim(u8, first[0..comment_pos], " \t");
|
|
}
|
|
|
|
var name: []const u8 = undefined;
|
|
var value: ?[]const u8 = null;
|
|
|
|
if (std.mem.indexOf(u8, name_part, "=")) |eq_pos| {
|
|
name = std.mem.trim(u8, name_part[0..eq_pos], " \t");
|
|
value = try self.allocator.dupe(u8, std.mem.trim(u8, name_part[eq_pos + 1 ..], " \t"));
|
|
} else {
|
|
name = name_part;
|
|
}
|
|
|
|
return EnumValue{
|
|
.name = fixupZigName(name),
|
|
.value = value,
|
|
.comment = comment,
|
|
};
|
|
}
|
|
|
|
// Pattern: typedef struct SDL_Foo { ... } SDL_Foo;
|
|
fn scanStruct(self: *Scanner) !?StructDecl {
|
|
const start = self.pos;
|
|
|
|
if (!self.matchPrefix("typedef struct ")) {
|
|
return null;
|
|
}
|
|
|
|
// Find the opening brace and extract the name before it
|
|
// But stop if we hit a semicolon (indicates forward declaration)
|
|
// Allow newlines/whitespace before the brace
|
|
const name_start = self.pos;
|
|
var found_semicolon = false;
|
|
while (self.pos < self.source.len and self.source[self.pos] != '{') {
|
|
if (self.source[self.pos] == ';') {
|
|
found_semicolon = true;
|
|
break;
|
|
}
|
|
self.pos += 1;
|
|
}
|
|
|
|
if (self.pos >= self.source.len or found_semicolon or self.source[self.pos] != '{') {
|
|
// No opening brace found - this is an opaque type or forward declaration
|
|
self.pos = start;
|
|
return null;
|
|
}
|
|
|
|
// Extract name from between "typedef struct " and "{"
|
|
const name_slice = std.mem.trim(u8, self.source[name_start..self.pos], " \t\n\r");
|
|
var iter = std.mem.tokenizeScalar(u8, name_slice, ' ');
|
|
const name = iter.next() orelse {
|
|
self.pos = start;
|
|
return null;
|
|
};
|
|
|
|
// Now we're at the opening brace, read the braced block
|
|
const body = try self.readBracedBlock();
|
|
defer self.allocator.free(body);
|
|
|
|
// Check if struct contains unions - C unions can't be represented in other languages
|
|
const has_unions = std.mem.indexOf(u8, body, "union ") != null or
|
|
std.mem.indexOf(u8, body, "union{") != null or
|
|
std.mem.indexOf(u8, body, "union\n") != null or
|
|
std.mem.indexOf(u8, body, "union\r") != null;
|
|
|
|
// Parse fields
|
|
var fields = try std.ArrayList(FieldDecl).initCapacity(self.allocator, 20);
|
|
var lines = std.mem.splitScalar(u8, body, '\n');
|
|
var in_multiline_comment = false;
|
|
|
|
while (lines.next()) |line| {
|
|
const trimmed = std.mem.trim(u8, line, " \t\r");
|
|
|
|
// Track multi-line comments (both /** and /*)
|
|
// Only start tracking if /* appears without */ on the same line
|
|
if (!in_multiline_comment) {
|
|
if (std.mem.indexOf(u8, trimmed, "/*")) |start_pos| {
|
|
if (std.mem.indexOf(u8, trimmed, "*/")) |_| {
|
|
// Both /* and */ on same line - it's an inline comment, not multi-line
|
|
// If line starts with /*, skip it entirely
|
|
if (start_pos == 0) continue;
|
|
// Otherwise it contains an inline comment, process the line normally
|
|
} else {
|
|
// Found /* without */ - start of multi-line comment
|
|
in_multiline_comment = true;
|
|
continue;
|
|
}
|
|
}
|
|
} else {
|
|
// We're in a multi-line comment, look for */
|
|
if (std.mem.indexOf(u8, trimmed, "*/") != null) {
|
|
in_multiline_comment = false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Skip comment/bracket/preprocessor lines
|
|
if (trimmed.len == 0) continue;
|
|
if (std.mem.startsWith(u8, trimmed, "//")) continue;
|
|
if (std.mem.startsWith(u8, trimmed, "*")) continue;
|
|
if (std.mem.startsWith(u8, trimmed, "#")) continue;
|
|
|
|
// First try single-field parsing
|
|
if (try self.parseStructField(line)) |field| {
|
|
try fields.append(self.allocator, field);
|
|
} else {
|
|
// If single-field fails, try multi-field parsing
|
|
const multi_fields = try self.parseMultiFieldLine(line);
|
|
if (multi_fields.len > 0) {
|
|
for (multi_fields) |field| {
|
|
try fields.append(self.allocator, field);
|
|
}
|
|
self.allocator.free(multi_fields);
|
|
}
|
|
}
|
|
}
|
|
|
|
const doc = self.consumePendingDocComment();
|
|
|
|
return StructDecl{
|
|
.name = try self.allocator.dupe(u8, name),
|
|
.fields = try fields.toOwnedSlice(self.allocator),
|
|
.doc_comment = doc,
|
|
.has_unions = has_unions,
|
|
};
|
|
}
|
|
|
|
fn scanUnion(self: *Scanner) !?UnionDecl {
|
|
const start = self.pos;
|
|
|
|
if (!self.matchPrefix("typedef union ")) {
|
|
return null;
|
|
}
|
|
|
|
// Find the opening brace and extract the name before it
|
|
// But stop if we hit a semicolon (indicates forward declaration)
|
|
// Allow newlines/whitespace before the brace
|
|
const name_start = self.pos;
|
|
var found_semicolon = false;
|
|
while (self.pos < self.source.len and self.source[self.pos] != '{') {
|
|
if (self.source[self.pos] == ';') {
|
|
found_semicolon = true;
|
|
break;
|
|
}
|
|
self.pos += 1;
|
|
}
|
|
|
|
if (self.pos >= self.source.len or found_semicolon or self.source[self.pos] != '{') {
|
|
// No opening brace found - this is an opaque type, not a union
|
|
self.pos = start;
|
|
return null;
|
|
}
|
|
|
|
// Extract name from between "typedef union " and "{"
|
|
const name_slice = std.mem.trim(u8, self.source[name_start..self.pos], " \t\n\r");
|
|
var iter = std.mem.tokenizeScalar(u8, name_slice, ' ');
|
|
const name = iter.next() orelse {
|
|
self.pos = start;
|
|
return null;
|
|
};
|
|
|
|
// Now we're at the opening brace, read the braced block
|
|
const body = try self.readBracedBlock();
|
|
defer self.allocator.free(body);
|
|
|
|
// Parse fields
|
|
var fields = try std.ArrayList(FieldDecl).initCapacity(self.allocator, 20);
|
|
var lines = std.mem.splitScalar(u8, body, '\n');
|
|
var in_multiline_comment = false;
|
|
|
|
while (lines.next()) |line| {
|
|
const trimmed = std.mem.trim(u8, line, " \t\r");
|
|
|
|
// Track multi-line comments (both /** and /*)
|
|
// Only start tracking if /* appears without */ on the same line
|
|
if (!in_multiline_comment) {
|
|
if (std.mem.indexOf(u8, trimmed, "/*")) |start_pos| {
|
|
if (std.mem.indexOf(u8, trimmed, "*/")) |_| {
|
|
// Both /* and */ on same line - it's an inline comment, not multi-line
|
|
// If line starts with /*, skip it entirely
|
|
if (start_pos == 0) continue;
|
|
// Otherwise it contains an inline comment, process the line normally
|
|
} else {
|
|
// Found /* without */ - start of multi-line comment
|
|
in_multiline_comment = true;
|
|
continue;
|
|
}
|
|
}
|
|
} else {
|
|
// We're in a multi-line comment, look for */
|
|
if (std.mem.indexOf(u8, trimmed, "*/") != null) {
|
|
in_multiline_comment = false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Skip comment/bracket/preprocessor lines
|
|
if (trimmed.len == 0) continue;
|
|
if (std.mem.startsWith(u8, trimmed, "//")) continue;
|
|
if (std.mem.startsWith(u8, trimmed, "*")) continue;
|
|
if (std.mem.startsWith(u8, trimmed, "#")) continue;
|
|
|
|
// Reuse struct field parsing since unions have same field syntax
|
|
if (try self.parseStructField(line)) |field| {
|
|
try fields.append(self.allocator, field);
|
|
} else {
|
|
const multi_fields = try self.parseMultiFieldLine(line);
|
|
if (multi_fields.len > 0) {
|
|
for (multi_fields) |field| {
|
|
try fields.append(self.allocator, field);
|
|
}
|
|
self.allocator.free(multi_fields);
|
|
}
|
|
}
|
|
}
|
|
|
|
const doc = self.consumePendingDocComment();
|
|
|
|
return UnionDecl{
|
|
.name = try self.allocator.dupe(u8, name),
|
|
.fields = try fields.toOwnedSlice(self.allocator),
|
|
.doc_comment = doc,
|
|
};
|
|
}
|
|
|
|
fn parseStructField(self: *Scanner, line: []const u8) !?FieldDecl {
|
|
const trimmed = std.mem.trim(u8, line, " \t\r");
|
|
if (trimmed.len == 0) return null;
|
|
if (std.mem.startsWith(u8, trimmed, "//")) return null;
|
|
if (std.mem.startsWith(u8, trimmed, "/*")) return null;
|
|
if (std.mem.startsWith(u8, trimmed, "{")) return null; // Skip opening brace
|
|
if (std.mem.startsWith(u8, trimmed, "}")) return null; // Skip closing brace and typedef name
|
|
|
|
// Remove trailing semicolon
|
|
const no_semi = std.mem.trimRight(u8, trimmed, ";");
|
|
|
|
// Extract inline comment
|
|
var comment: ?[]const u8 = null;
|
|
errdefer if (comment) |c| self.allocator.free(c);
|
|
|
|
var field_part = no_semi;
|
|
if (std.mem.indexOf(u8, no_semi, "/**<")) |comment_start| {
|
|
field_part = std.mem.trimRight(u8, no_semi[0..comment_start], "; \t");
|
|
if (std.mem.indexOf(u8, no_semi[comment_start..], "*/")) |end_offset| {
|
|
const comment_text = no_semi[comment_start + 4 .. comment_start + end_offset];
|
|
comment = try self.allocator.dupe(u8, std.mem.trim(u8, comment_text, " \t"));
|
|
}
|
|
}
|
|
|
|
// Check for function pointer field: RetType (SDLCALL *field_name)(params)
|
|
if (std.mem.indexOf(u8, field_part, "(SDLCALL *")) |sdlcall_pos| {
|
|
// Find the * after SDLCALL
|
|
const after_sdlcall = field_part[sdlcall_pos + 10 ..]; // Skip "(SDLCALL *"
|
|
if (std.mem.indexOf(u8, after_sdlcall, ")")) |close_paren| {
|
|
const field_name = std.mem.trim(u8, after_sdlcall[0..close_paren], " \t");
|
|
|
|
// The entire thing is the type (we'll convert to Zig function pointer syntax later)
|
|
return FieldDecl{
|
|
.name = fixupZigName(field_name),
|
|
.type_name = try self.allocator.dupe(u8, std.mem.trim(u8, field_part, " \t")),
|
|
.comment = comment,
|
|
};
|
|
}
|
|
} else if (std.mem.indexOf(u8, field_part, "(*")) |star_pos| {
|
|
// Handle non-SDLCALL function pointers: RetType (*field_name)(params)
|
|
const after_star = field_part[star_pos + 2 ..]; // Skip "(*"
|
|
if (std.mem.indexOf(u8, after_star, ")")) |close_paren| {
|
|
const field_name = std.mem.trim(u8, after_star[0..close_paren], " \t");
|
|
|
|
// The entire thing is the type (we'll convert to Zig function pointer syntax later)
|
|
return FieldDecl{
|
|
.name = fixupZigName(field_name),
|
|
.type_name = try self.allocator.dupe(u8, std.mem.trim(u8, field_part, " \t")),
|
|
.comment = comment,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Check if this line contains multiple comma-separated fields (e.g., "int x, y;")
|
|
// Only split on commas that are not inside nested structures (ignore for now)
|
|
const field_trimmed = std.mem.trim(u8, field_part, " \t");
|
|
|
|
// Simple heuristic: if there's a comma and no parentheses/brackets, it's multi-field
|
|
const has_comma = std.mem.indexOf(u8, field_trimmed, ",") != null;
|
|
const has_parens = std.mem.indexOf(u8, field_trimmed, "(") != null;
|
|
const has_brackets = std.mem.indexOf(u8, field_trimmed, "[") != null;
|
|
|
|
if (has_comma and !has_parens and !has_brackets) {
|
|
// This is a multi-field declaration like "int x, y"
|
|
// We'll return just the first field and rely on a helper to get the rest
|
|
// For now, return null and let the caller handle it with parseMultiFieldLine
|
|
if (comment) |c| self.allocator.free(c);
|
|
return null;
|
|
}
|
|
|
|
// Parse "type name" or "type name[size]" - handle pointer types and arrays correctly
|
|
// Examples:
|
|
// "SDL_GPUTransferBuffer *transfer_buffer" -> type:"SDL_GPUTransferBuffer *" name:"transfer_buffer"
|
|
// "Uint32 offset" -> type:"Uint32" name:"offset"
|
|
// "Uint8 padding[2]" -> type:"Uint8[2]" name:"padding"
|
|
|
|
// Check if this is an array field (has brackets)
|
|
if (std.mem.indexOf(u8, field_trimmed, "[")) |bracket_pos| {
|
|
// Extract array size and append to type
|
|
// Pattern: "Uint8 padding[2]" -> parse as type="Uint8[2]" name="padding"
|
|
const before_bracket = std.mem.trimRight(u8, field_trimmed[0..bracket_pos], " \t");
|
|
const bracket_part = field_trimmed[bracket_pos..]; // "[2]"
|
|
|
|
// Split before_bracket into type and name
|
|
var tokens = std.mem.tokenizeScalar(u8, before_bracket, ' ');
|
|
var parts_list: [8][]const u8 = undefined;
|
|
var parts_count: usize = 0;
|
|
while (tokens.next()) |token| {
|
|
if (token.len > 0 and !std.mem.eql(u8, token, "const")) {
|
|
if (parts_count >= 8) {
|
|
if (comment) |c| self.allocator.free(c);
|
|
return null;
|
|
}
|
|
parts_list[parts_count] = token;
|
|
parts_count += 1;
|
|
}
|
|
}
|
|
|
|
if (parts_count < 2) {
|
|
if (comment) |c| self.allocator.free(c);
|
|
return null; // Need at least type and name
|
|
}
|
|
|
|
const name = parts_list[parts_count - 1];
|
|
const type_parts = parts_list[0 .. parts_count - 1];
|
|
|
|
// Reconstruct type with array notation
|
|
var type_buf: [128]u8 = undefined;
|
|
var fbs = std.io.fixedBufferStream(&type_buf);
|
|
const writer = fbs.writer();
|
|
for (type_parts, 0..) |part, i| {
|
|
if (i > 0) writer.writeByte(' ') catch {
|
|
if (comment) |c| self.allocator.free(c);
|
|
return null;
|
|
};
|
|
writer.writeAll(part) catch {
|
|
if (comment) |c| self.allocator.free(c);
|
|
return null;
|
|
};
|
|
}
|
|
writer.writeAll(bracket_part) catch {
|
|
if (comment) |c| self.allocator.free(c);
|
|
return null;
|
|
};
|
|
|
|
const type_str = fbs.getWritten();
|
|
|
|
return FieldDecl{
|
|
.name = fixupZigName(name),
|
|
.type_name = try self.allocator.dupe(u8, type_str),
|
|
.comment = comment,
|
|
};
|
|
}
|
|
|
|
// Find last identifier by scanning backwards for alphanumeric/_
|
|
// The field name is the last contiguous sequence of [a-zA-Z0-9_]
|
|
var name_end: usize = field_trimmed.len;
|
|
var name_start: ?usize = null;
|
|
|
|
// Scan backwards to find the end of the last identifier (skip trailing whitespace)
|
|
while (name_end > 0) {
|
|
const c = field_trimmed[name_end - 1];
|
|
if (std.ascii.isAlphanumeric(c) or c == '_') {
|
|
break;
|
|
}
|
|
name_end -= 1;
|
|
}
|
|
|
|
// Now scan backwards from name_end to find where the identifier starts
|
|
if (name_end > 0) {
|
|
var i: usize = name_end;
|
|
while (i > 0) {
|
|
const c = field_trimmed[i - 1];
|
|
if (std.ascii.isAlphanumeric(c) or c == '_') {
|
|
i -= 1;
|
|
} else {
|
|
name_start = i;
|
|
break;
|
|
}
|
|
}
|
|
if (name_start == null and i == 0) {
|
|
name_start = 0;
|
|
}
|
|
}
|
|
|
|
if (name_start) |start| {
|
|
const name = field_trimmed[start..name_end];
|
|
const type_part = std.mem.trim(u8, field_trimmed[0..start], " \t");
|
|
|
|
if (name.len > 0 and type_part.len > 0) {
|
|
return FieldDecl{
|
|
.name = fixupZigName(name),
|
|
.type_name = try self.allocator.dupe(u8, type_part),
|
|
.comment = comment,
|
|
};
|
|
}
|
|
}
|
|
|
|
if (comment) |c| self.allocator.free(c);
|
|
return null;
|
|
}
|
|
|
|
// Parse multi-field declaration like "int x, y;" into separate fields
|
|
fn parseMultiFieldLine(self: *Scanner, line: []const u8) ![]FieldDecl {
|
|
const trimmed = std.mem.trim(u8, line, " \t\r");
|
|
if (trimmed.len == 0) return &[_]FieldDecl{};
|
|
if (std.mem.startsWith(u8, trimmed, "//")) return &[_]FieldDecl{};
|
|
if (std.mem.startsWith(u8, trimmed, "/*")) return &[_]FieldDecl{};
|
|
if (std.mem.startsWith(u8, trimmed, "{")) return &[_]FieldDecl{};
|
|
if (std.mem.startsWith(u8, trimmed, "}")) return &[_]FieldDecl{};
|
|
|
|
// Remove trailing semicolon
|
|
const no_semi = std.mem.trimRight(u8, trimmed, ";");
|
|
|
|
// Extract inline comment if present
|
|
var comment: ?[]const u8 = null;
|
|
var field_part = no_semi;
|
|
if (std.mem.indexOf(u8, no_semi, "/**<")) |comment_start| {
|
|
field_part = std.mem.trimRight(u8, no_semi[0..comment_start], "; \t");
|
|
if (std.mem.indexOf(u8, no_semi[comment_start..], "*/")) |end_offset| {
|
|
const comment_text = no_semi[comment_start + 4 .. comment_start + end_offset];
|
|
comment = try self.allocator.dupe(u8, std.mem.trim(u8, comment_text, " \t"));
|
|
}
|
|
}
|
|
defer if (comment) |c| self.allocator.free(c);
|
|
|
|
const field_trimmed = std.mem.trim(u8, field_part, " \t");
|
|
|
|
// Check if this is actually a multi-field line
|
|
const has_comma = std.mem.indexOf(u8, field_trimmed, ",") != null;
|
|
if (!has_comma) {
|
|
return &[_]FieldDecl{};
|
|
}
|
|
|
|
// Parse pattern: "type name1, name2, name3"
|
|
// Find where the type ends (last space before first comma)
|
|
const first_comma = std.mem.indexOf(u8, field_trimmed, ",") orelse return &[_]FieldDecl{};
|
|
|
|
// Everything before the first field name is the type
|
|
// Scan backwards from first comma to find where the first name starts
|
|
var type_end: usize = first_comma;
|
|
while (type_end > 0) {
|
|
const c = field_trimmed[type_end - 1];
|
|
if (c == ' ' or c == '\t' or c == '*') {
|
|
break;
|
|
}
|
|
type_end -= 1;
|
|
}
|
|
|
|
// Type is everything from start to type_end
|
|
const type_part = std.mem.trim(u8, field_trimmed[0..type_end], " \t");
|
|
|
|
if (type_part.len == 0) {
|
|
return &[_]FieldDecl{};
|
|
}
|
|
|
|
// Now parse the comma-separated field names
|
|
const names_part = field_trimmed[type_end..];
|
|
var field_list = std.ArrayList(FieldDecl){};
|
|
|
|
var name_iter = std.mem.splitScalar(u8, names_part, ',');
|
|
while (name_iter.next()) |name_raw| {
|
|
const name = std.mem.trim(u8, name_raw, " \t*");
|
|
if (name.len > 0) {
|
|
try field_list.append(self.allocator, FieldDecl{
|
|
.name = fixupZigName(name),
|
|
.type_name = try self.allocator.dupe(u8, type_part),
|
|
.comment = if (comment) |c| try self.allocator.dupe(u8, c) else null,
|
|
});
|
|
}
|
|
}
|
|
|
|
return try field_list.toOwnedSlice(self.allocator);
|
|
}
|
|
|
|
// Pattern: typedef Uint32 SDL_FooFlags;
|
|
fn scanFlagTypedef(self: *Scanner) !?FlagDecl {
|
|
const start = self.pos;
|
|
|
|
if (!self.matchPrefix("typedef ")) {
|
|
return null;
|
|
}
|
|
|
|
const line = try self.readLine();
|
|
defer self.allocator.free(line);
|
|
|
|
// Parse: "Uint32 SDL_GPUTextureUsageFlags;" (after "typedef " was consumed)
|
|
var iter = std.mem.tokenizeScalar(u8, line, ' ');
|
|
const underlying = iter.next() orelse {
|
|
self.pos = start;
|
|
return null;
|
|
};
|
|
const name = iter.next() orelse {
|
|
self.pos = start;
|
|
return null;
|
|
};
|
|
|
|
const clean_name = std.mem.trimRight(u8, name, ";");
|
|
if (!std.mem.endsWith(u8, clean_name, "Flags")) {
|
|
self.pos = start;
|
|
return null;
|
|
}
|
|
|
|
// Now collect following #define lines
|
|
var flags = try std.ArrayList(FlagValue).initCapacity(self.allocator, 10);
|
|
|
|
// Skip any whitespace/newlines before looking for #define
|
|
self.skipWhitespace();
|
|
|
|
// Look ahead for #define lines
|
|
while (!self.isAtEnd()) {
|
|
const define_start = self.pos;
|
|
if (!self.matchPrefix("#define ")) {
|
|
self.pos = define_start;
|
|
break;
|
|
}
|
|
|
|
const define_line = try self.readLine();
|
|
defer self.allocator.free(define_line);
|
|
|
|
if (try self.parseFlagDefine(define_line)) |flag| {
|
|
try flags.append(self.allocator, flag);
|
|
} else {
|
|
// Not a flag define, restore position
|
|
self.pos = define_start;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const doc = self.consumePendingDocComment();
|
|
|
|
return FlagDecl{
|
|
.name = fixupZigName(clean_name),
|
|
.underlying_type = try self.allocator.dupe(u8, underlying),
|
|
.flags = try flags.toOwnedSlice(self.allocator),
|
|
.doc_comment = doc,
|
|
};
|
|
}
|
|
|
|
fn parseFlagDefine(self: *Scanner, line: []const u8) !?FlagValue {
|
|
// Format after #define consumed: "SDL_GPU_TEXTUREUSAGE_SAMPLER (1u << 0) /**< comment */"
|
|
// Note: line doesn't include "#define" - it was already consumed by matchPrefix
|
|
|
|
// Split by whitespace and get first token (the flag name)
|
|
var parts = std.mem.tokenizeScalar(u8, line, ' ');
|
|
const name = parts.next() orelse return null;
|
|
|
|
// Collect the value part (everything until comment)
|
|
var value_parts = try std.ArrayList(u8).initCapacity(self.allocator, 32);
|
|
defer value_parts.deinit(self.allocator);
|
|
|
|
while (parts.next()) |part| {
|
|
if (std.mem.indexOf(u8, part, "/**<")) |_| break;
|
|
if (value_parts.items.len > 0) try value_parts.append(self.allocator, ' ');
|
|
try value_parts.appendSlice(self.allocator, part);
|
|
}
|
|
|
|
if (value_parts.items.len == 0) return null;
|
|
|
|
// Extract comment
|
|
var comment: ?[]const u8 = null;
|
|
if (std.mem.indexOf(u8, line, "/**<")) |comment_start| {
|
|
if (std.mem.indexOf(u8, line[comment_start..], "*/")) |end_offset| {
|
|
const comment_text = line[comment_start + 4 .. comment_start + end_offset];
|
|
comment = try self.allocator.dupe(u8, std.mem.trim(u8, comment_text, " \t"));
|
|
}
|
|
}
|
|
|
|
return FlagValue{
|
|
.name = try self.allocator.dupe(u8, name),
|
|
.value = try value_parts.toOwnedSlice(self.allocator),
|
|
.comment = comment,
|
|
};
|
|
}
|
|
|
|
// Pattern: extern SDL_DECLSPEC Type SDLCALL SDL_Name(...);
|
|
fn scanFunction(self: *Scanner) !?FunctionDecl {
|
|
if (!self.matchPrefix("extern SDL_DECLSPEC ")) {
|
|
return null;
|
|
}
|
|
|
|
// Collect the full function declaration (may span multiple lines)
|
|
var func_text = try std.ArrayList(u8).initCapacity(self.allocator, 256);
|
|
defer func_text.deinit(self.allocator);
|
|
|
|
// Keep reading until we find the semicolon
|
|
while (!self.isAtEnd()) {
|
|
const line = try self.readLine();
|
|
defer self.allocator.free(line);
|
|
|
|
try func_text.appendSlice(self.allocator, line);
|
|
try func_text.append(self.allocator, ' ');
|
|
|
|
if (std.mem.indexOfScalar(u8, line, ';')) |_| break;
|
|
}
|
|
|
|
// Parse: ReturnType SDLCALL FunctionName(params);
|
|
const doc = self.consumePendingDocComment();
|
|
var text = func_text.items;
|
|
|
|
// Strip format string attribute macros
|
|
const macros_to_strip = [_][]const u8{
|
|
"SDL_PRINTF_FORMAT_STRING ",
|
|
"SDL_WPRINTF_FORMAT_STRING ",
|
|
"SDL_SCANF_FORMAT_STRING ",
|
|
};
|
|
for (macros_to_strip) |macro| {
|
|
while (std.mem.indexOf(u8, text, macro)) |pos| {
|
|
// Create new string without the macro
|
|
const before = text[0..pos];
|
|
const after = text[pos + macro.len ..];
|
|
const new_text = try std.fmt.allocPrint(self.allocator, "{s}{s}", .{ before, after });
|
|
defer self.allocator.free(new_text);
|
|
|
|
// Replace func_text content
|
|
func_text.clearRetainingCapacity();
|
|
try func_text.appendSlice(self.allocator, new_text);
|
|
text = func_text.items;
|
|
}
|
|
}
|
|
|
|
// Strip vararg function macros from end (e.g., SDL_PRINTF_VARARG_FUNC(1))
|
|
const vararg_macros = [_][]const u8{
|
|
"SDL_PRINTF_VARARG_FUNC",
|
|
"SDL_PRINTF_VARARG_FUNCV",
|
|
"SDL_WPRINTF_VARARG_FUNC",
|
|
"SDL_SCANF_VARARG_FUNC",
|
|
"SDL_ACQUIRE",
|
|
"SDL_RELEASE",
|
|
};
|
|
for (vararg_macros) |macro| {
|
|
if (std.mem.indexOf(u8, text, macro)) |pos| {
|
|
// Find semicolon after this position
|
|
if (std.mem.indexOfScalarPos(u8, text, pos, ';')) |semi_pos| {
|
|
// Find the closing ) before semicolon
|
|
var paren_pos = semi_pos;
|
|
while (paren_pos > pos and text[paren_pos] != ')') : (paren_pos -= 1) {}
|
|
if (text[paren_pos] == ')') {
|
|
// Remove from macro to )
|
|
const before = text[0..pos];
|
|
const after = text[paren_pos + 1 ..];
|
|
const new_text = try std.fmt.allocPrint(self.allocator, "{s}{s}", .{ before, after });
|
|
defer self.allocator.free(new_text);
|
|
|
|
func_text.clearRetainingCapacity();
|
|
try func_text.appendSlice(self.allocator, new_text);
|
|
text = func_text.items;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find SDLCALL to split return type and function name
|
|
const sdlcall_pos = std.mem.indexOf(u8, text, "SDLCALL ") orelse return null;
|
|
const return_type_str = std.mem.trim(u8, text[0..sdlcall_pos], " \t\n");
|
|
const after_sdlcall = text[sdlcall_pos + 8 ..]; // Skip "SDLCALL "
|
|
|
|
// Find the function name (ends at '(')
|
|
const paren_pos = std.mem.indexOfScalar(u8, after_sdlcall, '(') orelse return null;
|
|
const func_name = std.mem.trim(u8, after_sdlcall[0..paren_pos], " \t\n*");
|
|
|
|
// Extract parameters (between '(' and ')')
|
|
const params_start = paren_pos + 1;
|
|
const params_end = std.mem.lastIndexOfScalar(u8, after_sdlcall, ')') orelse return null;
|
|
const params_str = std.mem.trim(u8, after_sdlcall[params_start..params_end], " \t\n");
|
|
|
|
// Parse parameters - split by comma and extract type/name pairs
|
|
const params = try self.parseParams(params_str);
|
|
|
|
const name = try self.allocator.dupe(u8, func_name);
|
|
const return_type = try self.allocator.dupe(u8, return_type_str);
|
|
|
|
return FunctionDecl{
|
|
.name = name,
|
|
.return_type = return_type,
|
|
.params = params,
|
|
.doc_comment = doc,
|
|
};
|
|
}
|
|
|
|
fn parseParams(self: *Scanner, params_str: []const u8) ![]ParamDecl {
|
|
if (params_str.len == 0 or std.mem.eql(u8, params_str, "void")) {
|
|
return &[_]ParamDecl{};
|
|
}
|
|
|
|
var params_list = try std.ArrayList(ParamDecl).initCapacity(self.allocator, 4);
|
|
defer params_list.deinit(self.allocator);
|
|
|
|
// Split by comma (simple version - doesn't handle function pointers yet)
|
|
var iter = std.mem.splitSequence(u8, params_str, ",");
|
|
while (iter.next()) |param| {
|
|
const trimmed = std.mem.trim(u8, param, " \t\n");
|
|
if (trimmed.len == 0) continue;
|
|
|
|
// Find the last identifier (parameter name)
|
|
// Handle array syntax like "char *argv[]" -> type:"char **" name:"argv"
|
|
var working_param = trimmed;
|
|
var is_array = false;
|
|
|
|
// Check for array brackets [] and remove them
|
|
if (std.mem.lastIndexOfScalar(u8, working_param, '[')) |bracket_pos| {
|
|
// Find matching ]
|
|
if (std.mem.indexOfScalar(u8, working_param[bracket_pos..], ']')) |_| {
|
|
is_array = true;
|
|
working_param = std.mem.trimRight(u8, working_param[0..bracket_pos], " \t");
|
|
}
|
|
}
|
|
|
|
// Find the parameter name - it's the last identifier that's not a keyword
|
|
// Start from the end and find the last word that's not 'const' or 'restrict'
|
|
var name_start: usize = 0;
|
|
var i = working_param.len;
|
|
var found_name = false;
|
|
|
|
// First, find the last identifier
|
|
while (i > 0 and !found_name) {
|
|
i -= 1;
|
|
const c = working_param[i];
|
|
if (c == ' ' or c == '*' or c == '\t') {
|
|
const potential_name = std.mem.trim(u8, working_param[i + 1 ..], " \t");
|
|
// Check if this is a C keyword (const, restrict, etc.)
|
|
if (!std.mem.eql(u8, potential_name, "const") and
|
|
!std.mem.eql(u8, potential_name, "restrict") and
|
|
potential_name.len > 0)
|
|
{
|
|
name_start = i + 1;
|
|
found_name = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!found_name and working_param.len > 0) {
|
|
// If we never found a separator, the whole thing might be the name
|
|
// Check if it's not a type keyword
|
|
if (!std.mem.eql(u8, working_param, "void")) {
|
|
name_start = 0; // Will be handled as type-only below
|
|
}
|
|
}
|
|
|
|
if (name_start == 0) {
|
|
// No space found - might be just a type (like "void")
|
|
try params_list.append(self.allocator, ParamDecl{
|
|
.name = "",
|
|
.type_name = try self.allocator.dupe(u8, working_param),
|
|
});
|
|
} else {
|
|
var param_type = std.mem.trim(u8, working_param[0..name_start], " \t");
|
|
var param_name = std.mem.trim(u8, working_param[name_start..], " \t");
|
|
|
|
// If param_name starts with *, it belongs to the type
|
|
// e.g., "SDL_GPUFence *const" and "*fences" should be "SDL_GPUFence *const *" and "fences"
|
|
var type_buf: [512]u8 = undefined;
|
|
while (param_name.len > 0 and param_name[0] == '*') {
|
|
const new_type = try std.fmt.bufPrint(&type_buf, "{s} *", .{param_type});
|
|
param_type = new_type;
|
|
param_name = std.mem.trimLeft(u8, param_name[1..], " \t");
|
|
}
|
|
|
|
// If this was an array parameter, convert pointer level
|
|
// e.g., "char *" becomes "[*c][*c]char" for argv[]
|
|
if (is_array) {
|
|
// For array parameters like argv[], we need pointer-to-pointer
|
|
// Input: "char *argv[]" -> after strip: "char *"
|
|
// Output type should be: "[*c][*c]char"
|
|
// But for simplicity in generated code, we can use the original type + pointer
|
|
// Check if type already ends with *
|
|
const trimmed_type = std.mem.trimRight(u8, param_type, " \t");
|
|
if (std.mem.endsWith(u8, trimmed_type, "*")) {
|
|
// Already has pointer, add another without space
|
|
const type_copy = try std.fmt.bufPrint(&type_buf, "{s}*", .{trimmed_type});
|
|
param_type = type_copy;
|
|
} else {
|
|
const type_copy = try std.fmt.bufPrint(&type_buf, "{s} *", .{param_type});
|
|
param_type = type_copy;
|
|
}
|
|
}
|
|
|
|
try params_list.append(self.allocator, ParamDecl{
|
|
.name = fixupZigName(param_name),
|
|
.type_name = try self.allocator.dupe(u8, param_type),
|
|
});
|
|
}
|
|
}
|
|
|
|
return try params_list.toOwnedSlice(self.allocator);
|
|
}
|
|
|
|
fn scanFunctionTODO(self: *Scanner) !?FunctionDecl {
|
|
if (!self.matchPrefix("extern SDL_DECLSPEC ")) {
|
|
return null;
|
|
}
|
|
|
|
// Read until we find the semicolon (may span multiple lines)
|
|
var func_text = try std.ArrayList(u8).initCapacity(self.allocator, 256);
|
|
defer func_text.deinit(self.allocator);
|
|
|
|
while (!self.isAtEnd()) {
|
|
const line = try self.readLine();
|
|
defer self.allocator.free(line);
|
|
|
|
try func_text.appendSlice(self.allocator, line);
|
|
try func_text.append(self.allocator, ' ');
|
|
|
|
if (std.mem.indexOfScalar(u8, line, ';')) |_| break;
|
|
}
|
|
|
|
// Parse: extern SDL_DECLSPEC ReturnType SDLCALL FunctionName(params);
|
|
// This is simplified - just extract the basics
|
|
const doc = self.consumePendingDocComment();
|
|
|
|
// For now, store the raw declaration
|
|
// We'll parse it properly in codegen
|
|
return FunctionDecl{
|
|
.name = try self.allocator.dupe(u8, "TODO"),
|
|
.return_type = try self.allocator.dupe(u8, "TODO"),
|
|
.params = &[_]ParamDecl{},
|
|
.doc_comment = doc,
|
|
};
|
|
}
|
|
|
|
// Utility functions
|
|
fn isAtEnd(self: *Scanner) bool {
|
|
return self.pos >= self.source.len;
|
|
}
|
|
|
|
fn matchPrefix(self: *Scanner, prefix: []const u8) bool {
|
|
if (self.pos + prefix.len > self.source.len) return false;
|
|
const slice = self.source[self.pos .. self.pos + prefix.len];
|
|
if (std.mem.eql(u8, slice, prefix)) {
|
|
self.pos += prefix.len;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
fn readLine(self: *Scanner) ![]const u8 {
|
|
const start = self.pos;
|
|
while (self.pos < self.source.len and self.source[self.pos] != '\n') {
|
|
self.pos += 1;
|
|
}
|
|
if (self.pos < self.source.len) self.pos += 1; // Skip newline
|
|
|
|
return self.allocator.dupe(u8, self.source[start .. self.pos - 1]);
|
|
}
|
|
|
|
fn skipLine(self: *Scanner) void {
|
|
while (self.pos < self.source.len and self.source[self.pos] != '\n') {
|
|
self.pos += 1;
|
|
}
|
|
if (self.pos < self.source.len) self.pos += 1; // Skip newline
|
|
}
|
|
|
|
fn skipWhitespace(self: *Scanner) void {
|
|
while (self.pos < self.source.len) {
|
|
const c = self.source[self.pos];
|
|
if (c == ' ' or c == '\t' or c == '\n' or c == '\r') {
|
|
self.pos += 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn readBracedBlock(self: *Scanner) ![]const u8 {
|
|
// Assumes we're at the opening brace or just after it
|
|
var depth: i32 = 0;
|
|
const start = self.pos;
|
|
var found_open = false;
|
|
|
|
while (self.pos < self.source.len) {
|
|
const c = self.source[self.pos];
|
|
if (c == '{') {
|
|
depth += 1;
|
|
found_open = true;
|
|
} else if (c == '}') {
|
|
depth -= 1;
|
|
if (found_open and depth == 0) {
|
|
self.pos += 1;
|
|
// Skip to end of line (to consume the typedef name)
|
|
self.skipLine();
|
|
return self.allocator.dupe(u8, self.source[start..self.pos]);
|
|
}
|
|
}
|
|
self.pos += 1;
|
|
}
|
|
|
|
return error.UnmatchedBrace;
|
|
}
|
|
|
|
fn peekDocComment(self: *Scanner) ?[]const u8 {
|
|
// Look for /** ... */ doc comments
|
|
const start = self.pos;
|
|
|
|
// Skip whitespace
|
|
while (self.pos < self.source.len) {
|
|
const c = self.source[self.pos];
|
|
if (c != ' ' and c != '\t' and c != '\n' and c != '\r') break;
|
|
self.pos += 1;
|
|
}
|
|
|
|
if (self.pos + 3 < self.source.len and
|
|
self.source[self.pos] == '/' and
|
|
self.source[self.pos + 1] == '*' and
|
|
self.source[self.pos + 2] == '*')
|
|
{
|
|
const comment_start = self.pos;
|
|
self.pos += 3;
|
|
|
|
// Find end
|
|
while (self.pos + 1 < self.source.len) {
|
|
if (self.source[self.pos] == '*' and self.source[self.pos + 1] == '/') {
|
|
self.pos += 2;
|
|
// Allocate and return a copy of the comment
|
|
return self.allocator.dupe(u8, self.source[comment_start..self.pos]) catch null;
|
|
}
|
|
self.pos += 1;
|
|
}
|
|
}
|
|
|
|
self.pos = start;
|
|
return null;
|
|
}
|
|
|
|
fn consumePendingDocComment(self: *Scanner) ?[]const u8 {
|
|
const comment = self.pending_doc_comment;
|
|
self.pending_doc_comment = null;
|
|
return comment;
|
|
}
|
|
};
|
|
|
|
test "scan opaque typedef" {
|
|
const source = "typedef struct SDL_GPUDevice SDL_GPUDevice;";
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const allocator = arena.allocator();
|
|
|
|
var scanner = Scanner.init(allocator, source);
|
|
const decls = try scanner.scan();
|
|
|
|
try std.testing.expectEqual(@as(usize, 1), decls.len);
|
|
try std.testing.expect(decls[0] == .opaque_type);
|
|
try std.testing.expectEqualStrings("SDL_GPUDevice", decls[0].opaque_type.name);
|
|
}
|
|
|
|
test "scan function declaration" {
|
|
const source =
|
|
\\extern SDL_DECLSPEC bool SDLCALL SDL_GPUSupportsShaderFormats(
|
|
\\ SDL_GPUShaderFormat format_flags,
|
|
\\ const char *name);
|
|
;
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const allocator = arena.allocator();
|
|
|
|
var scanner = Scanner.init(allocator, source);
|
|
const decls = try scanner.scan();
|
|
|
|
try std.testing.expectEqual(@as(usize, 1), decls.len);
|
|
try std.testing.expect(decls[0] == .function_decl);
|
|
const func = decls[0].function_decl;
|
|
try std.testing.expectEqualStrings("SDL_GPUSupportsShaderFormats", func.name);
|
|
try std.testing.expectEqualStrings("bool", func.return_type);
|
|
}
|
|
|
|
test "scan flag typedef with newline before defines" {
|
|
const source =
|
|
\\typedef Uint32 SDL_GPUTextureUsageFlags;
|
|
\\
|
|
\\#define SDL_GPU_TEXTUREUSAGE_SAMPLER (1u << 0)
|
|
\\#define SDL_GPU_TEXTUREUSAGE_COLOR_TARGET (1u << 1)
|
|
\\#define SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET (1u << 2)
|
|
;
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const allocator = arena.allocator();
|
|
|
|
var scanner = Scanner.init(allocator, source);
|
|
const decls = try scanner.scan();
|
|
|
|
try std.testing.expectEqual(@as(usize, 1), decls.len);
|
|
try std.testing.expect(decls[0] == .flag_decl);
|
|
const flag = decls[0].flag_decl;
|
|
try std.testing.expectEqualStrings("SDL_GPUTextureUsageFlags", flag.name);
|
|
try std.testing.expectEqualStrings("Uint32", flag.underlying_type);
|
|
try std.testing.expectEqual(@as(usize, 3), flag.flags.len);
|
|
try std.testing.expectEqualStrings("SDL_GPU_TEXTUREUSAGE_SAMPLER", flag.flags[0].name);
|
|
try std.testing.expectEqualStrings("SDL_GPU_TEXTUREUSAGE_COLOR_TARGET", flag.flags[1].name);
|
|
try std.testing.expectEqualStrings("SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET", flag.flags[2].name);
|
|
}
|
|
|
|
test "scan flag typedef with multiple blank lines" {
|
|
const source =
|
|
\\typedef Uint32 SDL_GPUBufferUsageFlags;
|
|
\\
|
|
\\
|
|
\\#define SDL_GPU_BUFFERUSAGE_VERTEX (1u << 0)
|
|
\\#define SDL_GPU_BUFFERUSAGE_INDEX (1u << 1)
|
|
;
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const allocator = arena.allocator();
|
|
|
|
var scanner = Scanner.init(allocator, source);
|
|
const decls = try scanner.scan();
|
|
|
|
try std.testing.expectEqual(@as(usize, 1), decls.len);
|
|
try std.testing.expect(decls[0] == .flag_decl);
|
|
const flag = decls[0].flag_decl;
|
|
try std.testing.expectEqual(@as(usize, 2), flag.flags.len);
|
|
}
|
|
|
|
test "scan flag typedef with comments before defines" {
|
|
const source =
|
|
\\typedef Uint32 SDL_GPUColorComponentFlags;
|
|
\\
|
|
\\/* Comment here */
|
|
;
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const allocator = arena.allocator();
|
|
|
|
var scanner = Scanner.init(allocator, source);
|
|
const decls = try scanner.scan();
|
|
|
|
// Should still parse the typedef even if no #defines follow
|
|
try std.testing.expectEqual(@as(usize, 1), decls.len);
|
|
try std.testing.expect(decls[0] == .flag_decl);
|
|
const flag = decls[0].flag_decl;
|
|
try std.testing.expectEqualStrings("SDL_GPUColorComponentFlags", flag.name);
|
|
// No flags found, but that's ok
|
|
try std.testing.expectEqual(@as(usize, 0), flag.flags.len);
|
|
}
|