23 KiB
Dependency Resolution Flow - Technical Deep Dive
Overview
This document traces the complete flow from parser entry point through dependency resolution to final output generation.
Flow Diagram
main()
↓
Parse Primary Header (SDL_gpu.h)
↓
Analyze Dependencies
↓
Extract Missing Types
↓
Combine Declarations
↓
Generate Output
Detailed Step-by-Step Flow
Phase 1: Parser Entry Point
File: src/parser.zig::main()
pub fn main() !void {
// 1. Setup
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
// 2. Parse command line arguments
const header_path = args[1];
var output_file: ?[]const u8 = null;
var mock_output_file: ?[]const u8 = null;
// 3. Read the primary header file
const source = try std.fs.cwd().readFileAlloc(
allocator,
header_path,
10 * 1024 * 1024
);
defer allocator.free(source);
Inputs:
- Command line:
zig build run -- SDL_gpu.h --output=gpu.zig - Header file contents read into memory
Outputs:
source: []const u8 - Full header file contentheader_path: []const u8 - Path for finding dependency headers
Phase 2: Primary Header Parsing
File: src/parser.zig::main() continued
// 4. Parse declarations from primary header
var scanner = patterns.Scanner.init(allocator, source);
const decls = try scanner.scan();
// decls is now: []Declaration containing:
// - 13 opaque types (GPUDevice, GPUTexture, etc.)
// - 24 enums
// - 35 structs
// - 3 flags
// - 94 functions
Process:
Scanner.init()creates scanner with allocator and sourcescanner.scan()iterates through source line by line- Tries each pattern: opaque, enum, struct, flags, function
- Builds array of
Declarationunion variants - Each declaration owns its strings (allocated from scanner's allocator)
Outputs:
decls: []Declaration - Array of 169 declarations from SDL_gpu.h
Phase 3: Dependency Analysis Entry
File: src/parser.zig::main() continued
// 5. Create dependency resolver
var resolver = dependency_resolver.DependencyResolver.init(allocator);
defer resolver.deinit();
// 6. Analyze declarations to find missing types
try resolver.analyze(decls);
What DependencyResolver.init() does:
pub fn init(allocator: Allocator) DependencyResolver {
return .{
.allocator = allocator,
.referenced_types = std.StringHashMap(void).init(allocator),
.defined_types = std.StringHashMap(void).init(allocator),
};
}
Creates two HashMaps:
defined_types: Types defined in primary headerreferenced_types: Types used in function signatures/struct fields
Phase 4: Type Collection
File: src/dependency_resolver.zig::DependencyResolver.analyze()
pub fn analyze(self: *DependencyResolver, decls: []const Declaration) !void {
try self.collectDefinedTypes(decls); // Step 4a
try self.collectReferencedTypes(decls); // Step 4b
}
Step 4a: Collect Defined Types
fn collectDefinedTypes(self: *DependencyResolver, decls: []const Declaration) !void {
for (decls) |decl| {
const type_name = switch (decl) {
.opaque_type => |o| o.name, // e.g., "SDL_GPUDevice"
.enum_decl => |e| e.name, // e.g., "SDL_GPUPrimitiveType"
.struct_decl => |s| s.name, // e.g., "SDL_GPUViewport"
.flag_decl => |f| f.name, // e.g., "SDL_GPUTextureUsageFlags"
.function_decl => continue, // Functions don't define types
};
try self.defined_types.put(type_name, {});
}
}
Result: defined_types HashMap contains:
SDL_GPUDevice -> {}
SDL_GPUTexture -> {}
SDL_GPUViewport -> {}
SDL_GPUPrimitiveType -> {}
... (166 more entries)
Step 4b: Collect Referenced Types
fn collectReferencedTypes(self: *DependencyResolver, decls: []const Declaration) !void {
for (decls) |decl| {
switch (decl) {
.function_decl => |func| {
// Scan return type
try self.scanType(func.return_type);
// Scan each parameter type
for (func.params) |param| {
try self.scanType(param.type_name);
}
},
.struct_decl => |struct_decl| {
// Scan each field type
for (struct_decl.fields) |field| {
try self.scanType(field.type_name);
}
},
else => {},
}
}
}
Example: Function signature processing
// C function:
bool SDL_WindowSupportsGPUSwapchain(SDL_GPUDevice *device, SDL_Window *window)
// Parser sees:
.function_decl = {
.return_type = "bool",
.params = [
{ .type_name = "SDL_GPUDevice *" },
{ .type_name = "SDL_Window *" }
]
}
For each type string, calls scanType():
Phase 5: Type Extraction & Normalization
File: src/dependency_resolver.zig::scanType()
fn scanType(self: *DependencyResolver, type_str: []const u8) !void {
// Extract base type from decorated string
const base_type = extractBaseType(type_str);
if (base_type.len > 0 and isSDLType(base_type)) {
// Only add if not already present (deduplicate)
if (!self.referenced_types.contains(base_type)) {
// Must own the string (type_str may be freed)
const owned = try self.allocator.dupe(u8, base_type);
try self.referenced_types.put(owned, {});
}
}
}
Example: Type Extraction Process
Input: "SDL_Window *"
Step-by-step through extractBaseType():
fn extractBaseType(type_str: []const u8) []const u8 {
var result = "SDL_Window *";
// Loop 1: Remove leading qualifiers
result = std.mem.trim(u8, result, " \t"); // "SDL_Window *"
// No leading "const", "?", "*", etc.
// Loop 2: Remove trailing qualifiers
result = std.mem.trim(u8, result, " \t"); // "SDL_Window *"
// Check trailing "*"
if (std.mem.endsWith(u8, result, "*")) {
result = result[0..result.len-1]; // "SDL_Window "
continue;
}
result = std.mem.trim(u8, result, " \t"); // "SDL_Window"
return "SDL_Window";
}
Output: "SDL_Window" (clean type name)
More Examples:
"?*SDL_GPUDevice" -> "SDL_GPUDevice"
"*const SDL_Rect" -> "SDL_Rect"
"SDL_GPUBuffer *const *" -> "SDL_GPUBuffer"
"[*c]const u8" -> "u8"
"SDL_FColor" -> "SDL_FColor"
SDL Type Detection
fn isSDLType(type_str: []const u8) bool {
// Check for SDL_ prefix
if (std.mem.startsWith(u8, type_str, "SDL_")) {
return true;
}
// Check known Zig-ified names
const known_types = [_][]const u8{
"Window", "Rect", "FColor", "FlipMode",
"PropertiesID", "Surface", ...
};
for (known_types) |known| {
if (std.mem.eql(u8, type_str, known)) {
return true;
}
}
return false; // Primitive type like "bool", "u32"
}
Result: referenced_types HashMap contains:
SDL_Window -> {}
SDL_Rect -> {}
SDL_FColor -> {}
SDL_FlipMode -> {}
SDL_PropertiesID -> {}
SDL_GPUShaderFormat -> {}
Phase 6: Missing Type Calculation
File: src/parser.zig::main() continued
// 7. Get missing types (referenced but not defined)
const missing_types = try resolver.getMissingTypes(allocator);
defer {
for (missing_types) |t| allocator.free(t);
allocator.free(missing_types);
}
File: src/dependency_resolver.zig::getMissingTypes()
pub fn getMissingTypes(self: *DependencyResolver, allocator: Allocator) ![][]const u8 {
var missing = std.ArrayList([]const u8){};
var it = self.referenced_types.keyIterator();
while (it.next()) |key| {
// Check if type is NOT in defined_types
if (!self.defined_types.contains(key.*)) {
// This is a missing type - need to find it
try missing.append(allocator, try allocator.dupe(u8, key.*));
}
}
return try missing.toOwnedSlice(allocator);
}
Logic:
referenced_types = {SDL_Window, SDL_Rect, SDL_FColor, ...}
defined_types = {SDL_GPUDevice, SDL_GPUTexture, ...}
missing_types = referenced_types - defined_types
= {SDL_Window, SDL_Rect, SDL_FColor, SDL_FlipMode,
SDL_PropertiesID, SDL_GPUShaderFormat}
Output: Array of 6 strings (owned by caller)
Phase 7: Include Header Parsing
File: src/parser.zig::main() continued
if (missing_types.len > 0) {
// 8. Parse #include directives from source
const includes = try dependency_resolver.parseIncludes(allocator, source);
defer {
for (includes) |inc| allocator.free(inc);
allocator.free(includes);
}
File: src/dependency_resolver.zig::parseIncludes()
pub fn parseIncludes(allocator: Allocator, source: []const u8) ![][]const u8 {
var includes = std.ArrayList([]const u8){};
var lines = std.mem.splitScalar(u8, source, '\n');
while (lines.next()) |line| {
const trimmed = std.mem.trim(u8, line, " \t\r");
// Match: #include <SDL3/SDL_something.h>
if (std.mem.startsWith(u8, trimmed, "#include <SDL3/")) {
const after_open = "#include <SDL3/".len;
if (std.mem.indexOf(u8, trimmed[after_open..], ">")) |end| {
const header_name = trimmed[after_open..][0..end];
try includes.append(allocator, try allocator.dupe(u8, header_name));
}
}
}
return try includes.toOwnedSlice(allocator);
}
Example: From SDL_gpu.h header:
#include <SDL3/SDL_stdinc.h>
#include <SDL3/SDL_pixels.h>
#include <SDL3/SDL_properties.h>
#include <SDL3/SDL_rect.h>
#include <SDL3/SDL_surface.h>
#include <SDL3/SDL_video.h>
Output: Array of strings:
["SDL_stdinc.h", "SDL_pixels.h", "SDL_properties.h",
"SDL_rect.h", "SDL_surface.h", "SDL_video.h"]
Phase 8: Dependency Type Extraction
File: src/parser.zig::main() continued
// 9. Determine header directory
const header_dir = std.fs.path.dirname(header_path) orelse ".";
// e.g., "../SDL/include/SDL3"
var dependency_decls = std.ArrayList(patterns.Declaration){};
defer {
for (dependency_decls.items) |dep_decl| {
freeDeclDeep(allocator, dep_decl);
}
dependency_decls.deinit(allocator);
}
// 10. For each missing type, search dependency headers
for (missing_types) |missing_type| {
var found = false;
// Try each included header
for (includes) |include| {
// 10a. Build full path
const dep_path = try std.fs.path.join(
allocator,
&[_][]const u8{ header_dir, include }
);
defer allocator.free(dep_path);
// e.g., "../SDL/include/SDL3/SDL_pixels.h"
// 10b. Read dependency header
const dep_source = std.fs.cwd().readFileAlloc(
allocator,
dep_path,
10 * 1024 * 1024
) catch continue; // Skip if can't read
defer allocator.free(dep_source);
// 10c. Extract type from this header
if (try dependency_resolver.extractTypeFromHeader(
allocator,
dep_source,
missing_type
)) |dep_decl| {
try dependency_decls.append(allocator, dep_decl);
std.debug.print(" ✓ Found {s} in {s}\n",
.{missing_type, include});
found = true;
break; // Found it, stop searching
}
}
if (!found) {
std.debug.print(" ⚠ Warning: Could not find {s}\n",
.{missing_type});
}
}
Search Algorithm:
For missing_type "SDL_Window":
Try SDL_stdinc.h -> Not found
Try SDL_pixels.h -> Not found
Try SDL_properties.h -> Not found
Try SDL_rect.h -> Not found
Try SDL_surface.h -> Not found
Try SDL_video.h -> FOUND! ✓
Phase 9: Type Extraction from Header
File: src/dependency_resolver.zig::extractTypeFromHeader()
pub fn extractTypeFromHeader(
allocator: Allocator,
header_source: []const u8,
type_name: []const u8, // e.g., "SDL_Window"
) !?Declaration {
// 1. Parse the entire dependency header
var scanner = patterns.Scanner.init(allocator, header_source);
const all_decls = try scanner.scan();
defer {
for (all_decls) |decl| {
freeDeclaration(allocator, decl);
}
allocator.free(all_decls);
}
// 2. Search for matching type
for (all_decls) |decl| {
const decl_name = switch (decl) {
.opaque_type => |o| o.name,
.enum_decl => |e| e.name,
.struct_decl => |s| s.name,
.flag_decl => |f| f.name,
else => continue,
};
// 3. Found it!
if (std.mem.eql(u8, decl_name, type_name)) {
// 4. Deep clone so caller owns it
return try cloneDeclaration(allocator, decl);
}
}
return null; // Not found in this header
}
Example: Searching SDL_video.h for SDL_Window
- Parse SDL_video.h → 50+ declarations
- Iterate through all declarations
- Find:
.opaque_type = { .name = "SDL_Window", ... } - Clone the declaration (deep copy all strings)
- Return the clone
- Free all the temporary declarations from parsing
Cloning Process:
fn cloneDeclaration(allocator: Allocator, decl: Declaration) !Declaration {
return switch (decl) {
.opaque_type => |o| .{
.opaque_type = .{
.name = try allocator.dupe(u8, o.name), // Own the string
.doc_comment = if (o.doc_comment) |doc|
try allocator.dupe(u8, doc) else null,
},
},
// ... similar for enum, struct, flags
};
}
Why clone? The parsed declarations from scanner.scan() are freed after this function returns. We need owned copies that live until code generation.
Phase 10: Declaration Combining
File: src/parser.zig::main() continued
// 11. Combine dependency declarations with primary
var all_decls = std.ArrayList(patterns.Declaration){};
defer all_decls.deinit(allocator);
// IMPORTANT: Dependencies FIRST!
try all_decls.appendSlice(allocator, dependency_decls.items);
try all_decls.appendSlice(allocator, decls);
Result: Combined array
all_decls = [
// Dependencies (4 items)
{ .struct_decl = SDL_FColor },
{ .enum_decl = SDL_FlipMode },
{ .struct_decl = SDL_Rect },
{ .opaque_type = SDL_Window },
// Primary header (169 items)
{ .opaque_type = SDL_GPUDevice },
{ .enum_decl = SDL_GPUPrimitiveType },
... (167 more)
]
Why dependencies first? Types must be defined before they're used. Since primary header references dependency types, dependencies must come first.
Phase 11: Code Generation
File: src/parser.zig::main() continued
// 12. Generate Zig code from all declarations
const output = try codegen.CodeGen.generate(allocator, all_decls.items);
defer allocator.free(output);
File: src/codegen.zig::CodeGen.generate() (simplified)
pub fn generate(allocator: Allocator, decls: []const Declaration) ![]const u8 {
var buf = std.ArrayList(u8){};
// Header
try buf.appendSlice(allocator, "pub const c = @import(\"c.zig\").c;\n\n");
// Generate each declaration
for (decls) |decl| {
switch (decl) {
.opaque_type => |o| {
try buf.appendSlice(allocator, "pub const ");
try buf.appendSlice(allocator, stripSDLPrefix(o.name));
try buf.appendSlice(allocator, " = opaque {};\n");
},
.struct_decl => |s| {
try generateStruct(allocator, &buf, s);
},
// ... other types
}
}
return try buf.toOwnedSlice(allocator);
}
Output (excerpt):
pub const c = @import("c.zig").c;
pub const FColor = extern struct {
r: f32,
g: f32,
b: f32,
a: f32,
};
pub const Window = opaque {};
pub const GPUDevice = opaque {
pub inline fn windowSupportsGPU(
gpudevice: *GPUDevice,
window: ?*Window, // ✓ Window is defined above!
) bool {
return c.SDL_WindowSupportsGPUDevice(gpudevice, window);
}
};
Phase 12: AST Validation & Formatting
File: src/parser.zig::main() continued
// 13. Parse generated code as Zig AST
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);
// 14. Check for syntax errors
if (ast.errors.len > 0) {
std.debug.print("\nError: {d} syntax errors\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) });
}
return error.InvalidSyntax;
}
// 15. Format using Zig's formatter
const formatted_output = try ast.renderAlloc(allocator);
defer allocator.free(formatted_output);
Why validate? Catch codegen bugs early. If generated code doesn't parse, we know immediately.
Why format? Zig's formatter ensures consistent style, proper indentation, and canonical formatting.
Phase 13: Output Writing
File: src/parser.zig::main() continued
// 16. Write to file or stdout
if (output_file) |file_path| {
try std.fs.cwd().writeFile(.{
.sub_path = file_path,
.data = formatted_output,
});
std.debug.print("Generated: {s}\n", .{file_path});
} else {
_ = try std.posix.write(std.posix.STDOUT_FILENO, formatted_output);
}
Memory Management Flow
Allocations
- Primary header source: Freed at end of main()
- Primary declarations: Freed at end of main() (with deep free)
- Dependency resolver HashMaps: Freed in resolver.deinit()
- HashMap keys (in referenced_types): Freed in resolver.deinit()
- Missing types array: Freed explicitly after use
- Includes array: Freed explicitly after use
- Dependency header sources: Freed immediately after extraction
- Temporary parsed declarations: Freed immediately in extractTypeFromHeader()
- Cloned dependency declarations: Freed at end of scope
- Generated output: Freed after writing
- Formatted output: Freed after writing
Ownership Rules
- Scanner owns strings during parsing (from its allocator)
- Cloned declarations own strings after extraction (allocated explicitly)
- HashMap owns keys in referenced_types (duped when inserted)
- Caller owns result of getMissingTypes(), parseIncludes()
Error Handling Flow
Errors That Fail
// Fatal errors - exit immediately
- File not found (primary header)
- Out of memory
- Invalid syntax in generated code (optional)
Errors That Warn
// Warnings - continue execution
- Dependency header not readable → continue with next header
- Type not found in any header → print warning, continue
- Struct parsing errors → generate partial output
Example Error Flow
Parse SDL_gpu.h
↓
Missing type: SDL_Window
↓
Try SDL_pixels.h → catch FileNotFound → continue
Try SDL_video.h → Success! → break
↓
Missing type: SDL_Unknown
↓
Try all headers → Not found → print warning
↓
Continue with partial results
Performance Characteristics
Time Complexity
- Primary parsing: O(n) where n = source lines
- Type collection: O(d) where d = declarations
- Missing type detection: O(r) where r = referenced types
- Type extraction: O(h × d) where h = headers, d = declarations per header
- Overall: O(n + d + r + h×d) ≈ O(n) for typical cases
Space Complexity
- Primary declarations: O(d)
- Dependency declarations: O(m) where m = missing types
- HashMaps: O(t) where t = total unique types
- Peak memory: ~2-5MB for SDL_gpu.h
Optimization Points
- Cache parsed headers - Currently re-parse for each missing type
- Early exit - Stop searching after finding type
- String interning - Deduplicate type name strings
- Lazy loading - Only parse dependencies if missing types detected
Testing the Flow
Unit Test Example
test "complete dependency flow" {
const source =
\\typedef struct SDL_Type SDL_Type;
\\extern void SDL_Func(SDL_External *param);
;
// Phase 1: Parse
var scanner = Scanner.init(allocator, source);
const decls = try scanner.scan();
// Phase 2: Analyze
var resolver = DependencyResolver.init(allocator);
defer resolver.deinit();
try resolver.analyze(decls);
// Phase 3: Get missing
const missing = try resolver.getMissingTypes(allocator);
defer allocator.free(missing);
// Verify: SDL_External is missing
try testing.expectEqual(@as(usize, 1), missing.len);
try testing.expectEqualStrings("SDL_External", missing[0]);
}
Integration Test
# Create test header with dependency
echo 'typedef struct Dep Dep;' > dep.h
echo '#include "dep.h"' > main.h
echo 'void func(Dep *d);' >> main.h
# Parse with dependency resolution
zig build run -- main.h --output=out.zig
# Verify output contains Dep
grep 'pub const Dep' out.zig
Summary
The dependency resolution flow is:
- Parse primary header → get declarations
- Analyze declarations → find referenced vs defined types
- Calculate missing = referenced - defined
- Extract #include directives from source
- Search dependency headers for missing types
- Clone found declarations (deep copy)
- Combine dependency + primary declarations
- Generate Zig code with all types
- Validate and format using Zig AST
- Output to file or stdout
Each phase has clear inputs/outputs, proper memory management, and graceful error handling.