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, }; 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 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 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| { try decls.append(self.allocator, .{ .flag_decl = flag_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 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 lines = std.mem.splitScalar(u8, body, '\n'); while (lines.next()) |line| { const trimmed = std.mem.trim(u8, line, " \t\r"); 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; // Skip opening brace line if (std.mem.startsWith(u8, trimmed, "}")) continue; // Skip closing brace and typedef name if (try self.parseEnumValue(trimmed)) |value| { try values.append(self.allocator, value); } } 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 */ 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 name and optional value var name: []const u8 = undefined; var value: ?[]const u8 = null; if (std.mem.indexOf(u8, first, "=")) |eq_pos| { name = std.mem.trim(u8, first[0..eq_pos], " \t"); value = try self.allocator.dupe(u8, std.mem.trim(u8, first[eq_pos + 1 ..], " \t")); } else { name = first; } // Extract inline comment if present var comment: ?[]const u8 = null; const remainder = parts.rest(); if (std.mem.indexOf(u8, remainder, "/**<")) |start| { if (std.mem.indexOf(u8, remainder[start..], "*/")) |end_offset| { const comment_text = remainder[start + 4 .. start + end_offset]; comment = try self.allocator.dupe(u8, std.mem.trim(u8, comment_text, " \t")); } } 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'); while (lines.next()) |line| { if (try self.parseStructField(line)) |field| { try fields.append(self.allocator, field); } } 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")); } } // Parse "type name" - find last space const field_trimmed = std.mem.trim(u8, field_part, " \t"); if (std.mem.lastIndexOfScalar(u8, field_trimmed, ' ')) |last_space| { const type_name = std.mem.trim(u8, field_trimmed[0..last_space], " \t"); const name = std.mem.trim(u8, field_trimmed[last_space + 1 ..], " \t"); if (name.len > 0 and type_name.len > 0) { return FieldDecl{ .name = try self.allocator.dupe(u8, name), .type_name = try self.allocator.dupe(u8, type_name), .comment = comment, }; } } return null; } // 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); }