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 ; 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); }