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