472 lines
18 KiB
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);
|
|
}
|