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

1062 lines
38 KiB
Zig

const std = @import("std");
const Allocator = std.mem.Allocator;
// Simple data structures to hold extracted declarations
pub const Declaration = union(enum) {
opaque_type: OpaqueType,
enum_decl: EnumDecl,
struct_decl: StructDecl,
flag_decl: FlagDecl,
function_decl: FunctionDecl,
typedef_decl: TypedefDecl,
};
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,
};
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 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.scanFlagTypedef()) |flag_decl| {
// Flag typedef must come before simple typedef
try decls.append(self.allocator, .{ .flag_decl = flag_decl });
} 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
const name2_clean = std.mem.trimRight(u8, name2, ";");
if (!std.mem.eql(u8, name1, name2_clean)) {
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, name1);
const doc = self.consumePendingDocComment();
return OpaqueType{
.name = 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 (also handled elsewhere)
if (std.mem.indexOf(u8, line, "struct ") != null or std.mem.indexOf(u8, line, "enum ") != null) {
self.pos = start;
return null;
}
// Skip function pointer typedefs (contain parentheses)
if (std.mem.indexOf(u8, line, "(") != null) {
self.pos = start;
return null;
}
// Parse: typedef Type Name;
const trimmed = std.mem.trim(u8, line, " \t\r\n");
const no_semi = std.mem.trimRight(u8, trimmed, ";");
// Split into tokens
var tokens = std.mem.tokenizeScalar(u8, no_semi, ' ');
_ = tokens.next(); // Skip "typedef"
const underlying_type = tokens.next() orelse {
self.pos = start;
return null;
};
const name = tokens.next() orelse {
self.pos = start;
return null;
};
// Make sure it's an SDL type
if (!std.mem.startsWith(u8, name, "SDL_")) {
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(),
};
}
// 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
const name_start = self.pos;
while (self.pos < self.source.len and self.source[self.pos] != '{') {
self.pos += 1;
}
if (self.pos >= self.source.len) {
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| {
const trimmed = std.mem.trim(u8, line, " \t\r");
if (trimmed.len == 0) continue;
// Track multi-line comments
if (std.mem.indexOf(u8, trimmed, "/**")) |_| {
in_multiline_comment = true;
}
if (in_multiline_comment) {
if (std.mem.indexOf(u8, trimmed, "*/")) |_| {
in_multiline_comment = false;
}
continue;
}
// Skip various comment/bracket/preprocessor lines
if (std.mem.startsWith(u8, trimmed, "//")) continue;
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 = try self.allocator.dupe(u8, 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
const name_start = self.pos;
while (self.pos < self.source.len and self.source[self.pos] != '{') {
self.pos += 1;
}
if (self.pos >= self.source.len) {
// No opening brace found - this is an opaque type, not a struct
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);
// 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
if (std.mem.indexOf(u8, trimmed, "/**")) |_| {
in_multiline_comment = true;
}
if (in_multiline_comment) {
if (std.mem.indexOf(u8, trimmed, "*/")) |_| {
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;
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,
};
}
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;
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 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
return null;
}
// Parse "type name" - handle pointer types correctly
// Examples:
// "SDL_GPUTransferBuffer *transfer_buffer" -> type:"SDL_GPUTransferBuffer *" name:"transfer_buffer"
// "Uint32 offset" -> type:"Uint32" name:"offset"
// 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 = try self.allocator.dupe(u8, name),
.type_name = try self.allocator.dupe(u8, type_part),
.comment = comment,
};
}
}
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"));
}
}
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 = try self.allocator.dupe(u8, 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 = try self.allocator.dupe(u8, 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();
const 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)
// Simple heuristic: last space or * separates type from name
var name_start: usize = 0;
var i = trimmed.len;
while (i > 0) {
i -= 1;
const c = trimmed[i];
if (c == ' ' or c == '*' or c == '\t') {
name_start = i + 1;
break;
}
}
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, trimmed),
});
} else {
const param_type = std.mem.trim(u8, trimmed[0..name_start], " \t");
const param_name = std.mem.trim(u8, trimmed[name_start..], " \t");
try params_list.append(self.allocator, ParamDecl{
.name = try self.allocator.dupe(u8, 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);
}