sdlparser-scrap/src/parser.zig

472 lines
18 KiB
Zig

const std = @import("std");
const patterns = @import("patterns.zig");
const codegen = @import("codegen.zig");
const dependency_resolver = @import("dependency_resolver.zig");
const json_serializer = @import("json_serializer.zig");
const io = @import("io.zig");
const header_cache = @import("header_cache.zig");
pub fn freeDecls(allocator: std.mem.Allocator, decls: []patterns.Declaration) void {
for (decls) |decl| {
freeDeclDeep(allocator, decl);
}
allocator.free(decls);
}
pub fn main() !void {
const allocator = std.heap.smp_allocator;
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
if (args.len < 2) {
std.debug.print("Usage: {s} <header-file> [--output=<output-file>] [--mocks=<mock-file>] [--generate-json=<json-file>] [--timestamp=<timestamp>] [--basedir=<directory>]\n", .{args[0]});
std.debug.print("Example: {s} ../SDL/include/SDL3/SDL_gpu.h --output=gpu.zig\n", .{args[0]});
std.debug.print(" {s} ../SDL/include/SDL3/SDL_gpu.h --output=gpu.zig --mocks=gpu_mock.c\n", .{args[0]});
std.debug.print(" {s} ../SDL/include/SDL3/SDL_gpu.h --generate-json=gpu.json\n", .{args[0]});
std.debug.print(" {s} ../SDL/include/SDL3/SDL_gpu.h > gpu.zig\n", .{args[0]});
std.debug.print(" {s} ../SDL/include/SDL3/SDL_gpu.h --basedir=/path/to/workdir\n", .{args[0]});
return error.MissingArgument;
}
const header_path = args[1];
var output_file: ?[]const u8 = null;
var mock_output_file: ?[]const u8 = null;
var json_output_file: ?[]const u8 = null;
var timestamp: ?i64 = null;
var basedir: ?[]const u8 = null;
// Parse additional flags
for (args[2..]) |arg| {
const output_prefix = "--output=";
const mocks_prefix = "--mocks=";
const json_prefix = "--generate-json=";
const timestamp_prefix = "--timestamp=";
const basedir_prefix = "--basedir=";
if (std.mem.startsWith(u8, arg, output_prefix)) {
output_file = arg[output_prefix.len..];
} else if (std.mem.startsWith(u8, arg, mocks_prefix)) {
mock_output_file = arg[mocks_prefix.len..];
} else if (std.mem.startsWith(u8, arg, json_prefix)) {
json_output_file = arg[json_prefix.len..];
} else if (std.mem.startsWith(u8, arg, timestamp_prefix)) {
timestamp = std.fmt.parseInt(i64, arg[timestamp_prefix.len..], 10) catch null;
} else if (std.mem.startsWith(u8, arg, basedir_prefix)) {
basedir = arg[basedir_prefix.len..];
} else {
std.debug.print("Error: Unknown argument '{s}'\n", .{arg});
std.debug.print("Usage: {s} <header-file> [--output=<output-file>] [--mocks=<mock-file>] [--generate-json=<json-file>] [--timestamp=<timestamp>] [--basedir=<directory>]\n", .{args[0]});
return error.InvalidArgument;
}
}
// Change working directory if --basedir is specified
if (basedir) |dir| {
try std.posix.chdir(dir);
std.debug.print("Changed working directory to: {s}\n\n", .{dir});
}
// Archive any existing debug directory before this run
archiveExistingDebugDir(allocator, timestamp);
std.debug.print("SDL3 Header Parser\n", .{});
std.debug.print("==================\n\n", .{});
std.debug.print("Parsing: {s}\n\n", .{header_path});
// Read the header file
const source = try std.fs.cwd().readFileAlloc(allocator, header_path, 10 * 1024 * 1024); // 10MB max
defer allocator.free(source);
// Parse declarations
var scanner = patterns.Scanner.init(allocator, source);
const decls = try scanner.scan();
defer freeDecls(allocator, decls);
std.debug.print("Found {d} declarations\n", .{decls.len});
// Count each type
var opaque_count: usize = 0;
var typedef_count: usize = 0;
var func_ptr_count: usize = 0;
var c_type_alias_count: usize = 0;
var enum_count: usize = 0;
var struct_count: usize = 0;
var union_count: usize = 0;
var flag_count: usize = 0;
var func_count: usize = 0;
for (decls) |decl| {
switch (decl) {
.opaque_type => opaque_count += 1,
.typedef_decl => typedef_count += 1,
.function_pointer_decl => func_ptr_count += 1,
.c_type_alias => c_type_alias_count += 1,
.enum_decl => enum_count += 1,
.struct_decl => struct_count += 1,
.union_decl => union_count += 1,
.flag_decl => flag_count += 1,
.function_decl => func_count += 1,
}
}
std.debug.print(" - Opaque types: {d}\n", .{opaque_count});
std.debug.print(" - Typedefs: {d}\n", .{typedef_count});
std.debug.print(" - Function pointers: {d}\n", .{func_ptr_count});
std.debug.print(" - C type aliases: {d}\n", .{c_type_alias_count});
std.debug.print(" - Enums: {d}\n", .{enum_count});
std.debug.print(" - Structs: {d}\n", .{struct_count});
std.debug.print(" - Unions: {d}\n", .{union_count});
std.debug.print(" - Flags: {d}\n", .{flag_count});
std.debug.print(" - Functions: {d}\n\n", .{func_count});
// Generate JSON if requested
if (json_output_file) |json_path| {
std.debug.print("Generating JSON output...\n", .{});
var serializer = json_serializer.JsonSerializer.init(allocator, std.fs.path.basename(header_path));
try serializer.addDeclarations(decls);
const json_output = try serializer.finalize();
// json_output is owned by serializer
// Parse and re-format JSON with proper indentation
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, json_output, .{});
defer parsed.deinit();
var formatted_output = std.ArrayList(u8){};
defer formatted_output.deinit(allocator);
const formatter = std.json.fmt(parsed.value, .{ .whitespace = .indent_2 });
try std.fmt.format(formatted_output.writer(allocator), "{f}", .{formatter});
try ensureParentDirExists(json_path);
try std.fs.cwd().writeFile(.{
.sub_path = json_path,
.data = formatted_output.items,
});
serializer.deinit();
std.debug.print("Generated JSON: {s}\n", .{json_path});
// If only JSON was requested, we're done
if (output_file == null and mock_output_file == null) {
return;
}
}
// Analyze dependencies
std.debug.print("Analyzing dependencies...\n", .{});
var resolver = dependency_resolver.DependencyResolver.init(allocator);
defer resolver.deinit();
try resolver.analyze(decls);
const missing_types = try resolver.getMissingTypes(allocator);
defer {
for (missing_types) |t| allocator.free(t);
allocator.free(missing_types);
}
// Get hardcoded declarations for special types (like SDL_HitTest)
// const hardcoded_decls = try resolver.getHardcodedDeclarations(allocator);
// defer {
// for (hardcoded_decls) |hd| {
// freeDeclDeep(allocator, hd);
// }
// allocator.free(hardcoded_decls);
// }
// if (hardcoded_decls.len > 0) {
// std.debug.print("Adding {d} hardcoded type declarations\n", .{hardcoded_decls.len});
// }
var dependency_decls = std.ArrayList(patterns.Declaration){};
defer {
for (dependency_decls.items) |dep_decl| {
freeDeclDeep(allocator, dep_decl);
}
dependency_decls.deinit(allocator);
}
if (missing_types.len > 0) {
std.debug.print("Found {d} missing types:\n", .{missing_types.len});
for (missing_types) |missing| {
std.debug.print(" - {s}\n", .{missing});
}
std.debug.print("\n", .{});
// PHASE 1: Build complete type cache from all SDL headers
std.debug.print("Building type cache from all SDL headers...\n", .{});
var cache = try header_cache.HeaderCache.buildCache(allocator, header_path);
defer cache.deinit();
std.debug.print("Type cache built successfully\n\n", .{});
// PHASE 2: Resolve dependencies via cache lookup with recursive dependency resolution
std.debug.print("Resolving dependencies from cache...\n", .{});
// Track which types we've already added to avoid duplicates
var added_types = std.StringHashMap(void).init(allocator);
defer {
var it = added_types.keyIterator();
while (it.next()) |key| {
allocator.free(key.*);
}
added_types.deinit();
}
for (missing_types) |missing_type| {
// Skip if already added
if (added_types.contains(missing_type)) continue;
// Look up type with all its dependencies
const resolved_decls = cache.lookupTypeWithDependencies(missing_type) catch |err| {
std.debug.print(" Warning: Could not resolve {s}: {}\n", .{ missing_type, err });
continue;
};
// Add all resolved declarations
for (resolved_decls) |resolved_decl| {
const decl_type_name = switch (resolved_decl) {
.opaque_type => |o| o.name,
.typedef_decl => |t| t.name,
.function_pointer_decl => |fp| fp.name,
.c_type_alias => |a| a.name,
.enum_decl => |e| e.name,
.struct_decl => |s| s.name,
.union_decl => |u| u.name,
.flag_decl => |f| f.name,
.function_decl => continue, // Skip functions
};
if (!added_types.contains(decl_type_name)) {
try dependency_decls.append(allocator, resolved_decl);
try added_types.put(try allocator.dupe(u8, decl_type_name), {});
if (std.mem.eql(u8, decl_type_name, missing_type)) {
std.debug.print(" Found {s} in cache\n", .{missing_type});
} else {
std.debug.print(" + dependency: {s}\n", .{decl_type_name});
}
} else {
// Already added, free this duplicate
freeDeclDeep(allocator, resolved_decl);
}
}
allocator.free(resolved_decls);
}
std.debug.print("\n", .{});
}
// Combine declarations (hardcoded first, then dependencies, then primary!)
std.debug.print("Combining {d} dependency declarations with primary declarations...\n", .{dependency_decls.items.len});
var all_decls = std.ArrayList(patterns.Declaration){};
defer all_decls.deinit(allocator);
// try all_decls.appendSlice(allocator, hardcoded_decls);
try all_decls.appendSlice(allocator, dependency_decls.items);
try all_decls.appendSlice(allocator, decls);
// Generate code with all declarations
const output = try codegen.CodeGen.generate(allocator, all_decls.items);
defer allocator.free(output);
// Parse and format the AST for validation
const output_z = try allocator.dupeZ(u8, output);
defer allocator.free(output_z);
var ast = try std.zig.Ast.parse(allocator, output_z, .zig);
defer ast.deinit(allocator);
// Check for parse errors
if (ast.errors.len > 0) {
std.debug.print("{s}", .{output_z});
std.debug.print("\nError: {d} syntax errors detected in generated code\n", .{ast.errors.len});
for (ast.errors) |err| {
const loc = ast.tokenLocation(0, err.token);
std.debug.print(" Line {d}: {s}\n", .{ loc.line + 1, @tagName(err.tag) });
}
// Write to debug file
try writeDebugFile(allocator, header_path, output);
// Write unformatted output for debugging
if (output_file) |file_path| {
try std.fs.cwd().writeFile(.{
.sub_path = file_path,
.data = output,
});
std.debug.print("\nGenerated (with errors): {s}\n", .{file_path});
}
return error.InvalidSyntax;
}
// Render formatted output from AST
const formatted_output = ast.renderAlloc(allocator) catch |err| {
std.debug.print("\nError: AST render failed: {}\n", .{err});
try writeDebugFile(allocator, header_path, output);
return err;
};
defer allocator.free(formatted_output);
// Write formatted output to file or stdout
if (output_file) |file_path| {
try writeZigFileWithFmt(allocator, header_path, file_path, formatted_output);
} else {
_ = try io.stdout().write(formatted_output);
}
// Generate C mocks if requested (with all declarations)
if (mock_output_file) |mock_path| {
const mock_codegen = @import("mock_codegen.zig");
const mock_output = try mock_codegen.MockCodeGen.generate(allocator, all_decls.items);
defer allocator.free(mock_output);
try ensureParentDirExists(mock_path);
try std.fs.cwd().writeFile(.{
.sub_path = mock_path,
.data = mock_output,
});
std.debug.print("Generated C mocks: {s}\n", .{mock_path});
}
}
fn writeZigFileWithFmt(allocator: std.mem.Allocator, header_path: []const u8, output_path: []const u8, content: []const u8) !void {
const header_basename = std.fs.path.basename(header_path);
const output_basename = std.fs.path.basename(output_path);
// Create tmp directory
std.fs.cwd().makePath("tmp") catch |err| switch (err) {
error.PathAlreadyExists => {},
else => return err,
};
// Write to tmp file
const tmp_path = try std.fmt.allocPrint(allocator, "tmp/{s}", .{output_basename});
defer allocator.free(tmp_path);
try std.fs.cwd().writeFile(.{
.sub_path = tmp_path,
.data = content,
});
// Run zig ast-check on tmp file
var ast_check = std.process.Child.init(&.{ "zig", "ast-check", tmp_path }, allocator);
ast_check.stderr_behavior = .Pipe;
try ast_check.spawn();
const ast_result = try ast_check.wait();
if (ast_result.Exited != 0) {
// zig ast-check failed - copy to debug folder
std.fs.cwd().makePath("debug") catch |err| switch (err) {
error.PathAlreadyExists => {},
else => return err,
};
// Convert header name: SDL_gpu.h -> SDL_gpu_h
var safe_name = try allocator.alloc(u8, header_basename.len);
defer allocator.free(safe_name);
for (header_basename, 0..) |c, i| {
safe_name[i] = if (c == '.') '_' else c;
}
const debug_path = try std.fmt.allocPrint(allocator, "debug/{s}_fmterror.zig", .{safe_name});
defer allocator.free(debug_path);
try std.fs.cwd().writeFile(.{
.sub_path = debug_path,
.data = content,
});
std.debug.print("zig ast-check failed, debug output written to: {s}\n", .{debug_path});
return error.ZigAstCheckFailed;
}
// Run zig fmt on tmp file
var fmt = std.process.Child.init(&.{ "zig", "fmt", tmp_path }, allocator);
fmt.stderr_behavior = .Pipe;
try fmt.spawn();
_ = try fmt.wait();
// Copy formatted file to final destination
try ensureParentDirExists(output_path);
const formatted_content = try std.fs.cwd().readFileAlloc(allocator, tmp_path, 10 * 1024 * 1024);
defer allocator.free(formatted_content);
try std.fs.cwd().writeFile(.{
.sub_path = output_path,
.data = formatted_content,
});
std.debug.print("Generated: {s}\n", .{output_path});
}
fn ensureParentDirExists(path: []const u8) !void {
if (std.fs.path.dirname(path)) |dir| {
std.fs.cwd().makePath(dir) catch |err| switch (err) {
error.PathAlreadyExists => {},
else => return err,
};
}
}
fn archiveExistingDebugDir(allocator: std.mem.Allocator, provided_timestamp: ?i64) void {
// Check if debug/ directory exists
var debug_dir = std.fs.cwd().openDir("debug", .{}) catch return;
debug_dir.close();
// Use provided timestamp or generate one
const timestamp = provided_timestamp orelse std.time.timestamp();
const archive_path = std.fmt.allocPrint(allocator, "archive/debug/{d}", .{timestamp}) catch return;
defer allocator.free(archive_path);
// Create archive/debug directory if needed
std.fs.cwd().makePath("archive/debug") catch return;
// Move debug/ to archive/debug/<timestamp>/
std.fs.cwd().rename("debug", archive_path) catch |err| {
std.debug.print("Warning: Failed to archive existing debug dir: {}\n", .{err});
return;
};
std.debug.print("Archived existing debug/ to {s}\n", .{archive_path});
}
fn writeDebugFile(allocator: std.mem.Allocator, header_path: []const u8, output: []const u8) !void {
// Get the header basename (e.g., "SDL_gpu.h")
const header_basename = std.fs.path.basename(header_path);
// Convert to safe filename: SDL_gpu.h -> error_SDL_gpu_h.zig
var safe_name = try allocator.alloc(u8, header_basename.len);
defer allocator.free(safe_name);
for (header_basename, 0..) |c, i| {
safe_name[i] = if (c == '.') '_' else c;
}
const debug_filename = try std.fmt.allocPrint(allocator, "debug/error_{s}.zig", .{safe_name});
defer allocator.free(debug_filename);
// Create debug directory if it doesn't exist
std.fs.cwd().makeDir("debug") catch |err| switch (err) {
error.PathAlreadyExists => {},
else => return err,
};
try std.fs.cwd().writeFile(.{
.sub_path = debug_filename,
.data = output,
});
std.debug.print("Debug output written to: {s}\n", .{debug_filename});
}
fn freeDeclDeep(allocator: std.mem.Allocator, decl: patterns.Declaration) void {
switch (decl) {
inline else => |*d| d.deinit(allocator),
}
}
test "basic test" {
try std.testing.expect(true);
}