19 KiB
Dependency Resolution Implementation Plan (v2)
Core Insight
Single-file generation with on-demand type resolution: Parse the primary header completely, identify missing types, then parse included headers ONLY for those specific types. Append them to the same output file. Zig's structural typing handles everything else.
Why This Works
- No modules needed - One output file with all types
- Zig deduplicates - Multiple parsings of same type are safe
- Forward references work - Zig allows using types before they're defined
- Simple implementation - Just append to existing output
Three-Phase Algorithm
Phase 1: Primary Header Parsing (Existing)
Input: SDL_gpu.h
Output: declarations[], generated_code
Phase 2: Missing Type Detection (NEW)
1. Scan generated_code for all type references
2. Build set of defined_types from declarations[]
3. missing_types = referenced_types - defined_types
Phase 3: Dependency Resolution (NEW)
For each missing_type:
1. Parse each included header
2. If type found, extract declaration
3. Append to output
Implementation Details
Implementation Details
Component 1: Type Reference Scanner
Purpose: Find all type names used in generated code
Location: New file src/dependency_resolver.zig
pub const TypeReference = struct {
name: []const u8,
source_location: []const u8, // For debugging
};
pub fn scanTypeReferences(decls: []const Declaration) ![]TypeReference {
var refs = ArrayList(TypeReference).init(allocator);
for (decls) |decl| {
switch (decl) {
.function_decl => |func| {
// Scan return type
try scanType(func.return_type, &refs);
// Scan parameters
for (func.params) |param| {
try scanType(param.type_name, &refs);
}
},
.struct_decl => |struct_decl| {
// Scan field types
for (struct_decl.fields) |field| {
try scanType(field.type_name, &refs);
}
},
// opaque/enum don't reference other types
else => {},
}
}
return refs.toOwnedSlice();
}
fn scanType(type_str: []const u8, refs: *ArrayList(TypeReference)) !void {
// Extract base type from "?*const SDL_Type" → "SDL_Type"
const base_type = extractBaseType(type_str);
if (isSDLType(base_type)) {
try refs.append(.{ .name = base_type, .source_location = type_str });
}
}
Key functions:
extractBaseType(): Strip pointers, const, optional from type stringisSDLType(): Check if starts with "SDL_" or is known SDL type- Handle edge cases: arrays, function pointers (skip for now)
Component 2: Defined Type Collector
Purpose: Track what types are already defined
pub fn collectDefinedTypes(decls: []const Declaration) StringHashMap(void) {
var defined = StringHashMap(void).init(allocator);
for (decls) |decl| {
const type_name = switch (decl) {
.opaque_type => |o| o.name,
.enum_decl => |e| e.name,
.struct_decl => |s| s.name,
.flags_decl => |f| f.name,
.function_decl => continue, // Functions don't define types
};
try defined.put(type_name, {});
}
return defined;
}
Component 3: Include Header Parser
Purpose: Extract #include directives
pub fn parseIncludes(source: []const u8) ![]const []const u8 {
var includes = ArrayList([]const u8).init(allocator);
var lines = std.mem.split(u8, source, "\n");
while (lines.next()) |line| {
// Match: #include <SDL3/SDL_something.h>
if (std.mem.indexOf(u8, line, "#include <SDL3/")) |start| {
const after_open = start + "#include <SDL3/".len;
if (std.mem.indexOf(u8, line[after_open..], ">")) |end| {
const header_name = line[after_open..][0..end];
try includes.append(try allocator.dupe(u8, header_name));
}
}
}
return includes.toOwnedSlice();
}
Component 4: Selective Type Extractor
Purpose: Find specific type in a header
pub fn extractTypeFromHeader(
allocator: Allocator,
header_source: []const u8,
type_name: []const u8, // e.g., "SDL_Rect"
) !?Declaration {
// Parse the header
var scanner = Scanner.init(allocator, header_source);
const all_decls = try scanner.scan();
defer scanner.deinit();
// Find matching declaration
for (all_decls) |decl| {
const decl_name = switch (decl) {
.opaque_type => |o| o.name,
.enum_decl => |e| e.name,
.struct_decl => |s| s.name,
.flags_decl => |f| f.name,
else => continue,
};
if (std.mem.eql(u8, decl_name, type_name)) {
return try decl.clone(allocator); // Deep copy
}
}
return null; // Not found
}
Component 5: Main Integration
Location: Modify src/parser.zig main function
pub fn main() !void {
// ... existing setup ...
// 1. Parse primary header (existing)
var scanner = Scanner.init(allocator, source);
const primary_decls = try scanner.scan();
// 2. Identify missing types (NEW)
const references = try scanTypeReferences(primary_decls);
const defined = collectDefinedTypes(primary_decls);
var missing = ArrayList([]const u8).init(allocator);
for (references) |ref| {
if (!defined.contains(ref.name)) {
try missing.append(ref.name);
}
}
// 3. Extract missing types from dependencies (NEW)
var dependency_decls = ArrayList(Declaration).init(allocator);
if (missing.items.len > 0) {
const includes = try parseIncludes(source);
const header_dir = std.fs.path.dirname(header_path) orelse ".";
for (missing.items) |missing_type| {
var found = false;
for (includes) |include| {
const dep_path = try std.fs.path.join(
allocator,
&[_][]const u8{ header_dir, include }
);
defer allocator.free(dep_path);
const dep_source = std.fs.cwd().readFileAlloc(
allocator,
dep_path,
10 * 1024 * 1024
) catch continue;
defer allocator.free(dep_source);
if (try extractTypeFromHeader(allocator, dep_source, missing_type)) |decl| {
try dependency_decls.append(decl);
found = true;
break;
}
}
if (!found) {
std.debug.print(
"Warning: Could not find definition for type: {s}\n",
.{missing_type}
);
}
}
}
// 4. Combine declarations
var all_decls = ArrayList(Declaration).init(allocator);
try all_decls.appendSlice(dependency_decls.items); // Dependencies first!
try all_decls.appendSlice(primary_decls);
// 5. Generate code (existing, but with all declarations)
const output = try codegen.generate(allocator, all_decls.items);
// ... rest of existing code ...
}
Key Implementation Decisions
-
Dependencies go FIRST in output
- Ensures types are defined before use
- More logical reading order
-
Deep copy declarations
- Avoid lifetime issues with parsed headers
- Each declaration owns its strings
-
Warning for missing types
- Don't fail build, just warn
- Allows gradual improvement
-
Skip function pointers/unions
- Add TODO comments
- Focus on common cases first
-
Cache parsed headers
- Parse each dependency header once
- Extract multiple types from same parse
Testing Approach
Unit Tests
test "scanTypeReferences finds SDL types" {
const decls = [_]Declaration{
.{ .function_decl = .{
.name = "test",
.return_type = "?*SDL_Window",
.params = &[_]Param{
.{ .name = "rect", .type_name = "*const SDL_Rect" },
},
}},
};
const refs = try scanTypeReferences(&decls);
try testing.expect(refs.len == 2);
try testing.expectEqualStrings("SDL_Window", refs[0].name);
try testing.expectEqualStrings("SDL_Rect", refs[1].name);
}
test "collectDefinedTypes tracks declarations" {
const decls = [_]Declaration{
.{ .opaque_type = .{ .name = "SDL_Device" }},
.{ .struct_decl = .{ .name = "SDL_Info", .fields = &[_]Field{} }},
};
const defined = collectDefinedTypes(&decls);
try testing.expect(defined.contains("SDL_Device"));
try testing.expect(defined.contains("SDL_Info"));
}
Integration Test
test "full dependency resolution with SDL_gpu.h" {
const source = try std.fs.cwd().readFileAlloc(
testing.allocator,
"SDL/include/SDL3/SDL_gpu.h",
10 * 1024 * 1024
);
defer testing.allocator.free(source);
// Parse and resolve
const output = try parseWithDependencies(testing.allocator, source, "SDL/include/SDL3");
defer testing.allocator.free(output);
// Verify missing types are present
try testing.expect(std.mem.indexOf(u8, output, "pub const Window = opaque") != null);
try testing.expect(std.mem.indexOf(u8, output, "pub const Rect = extern struct") != null);
try testing.expect(std.mem.indexOf(u8, output, "pub const FColor = extern struct") != null);
// Verify it compiles
var ast = try std.zig.Ast.parse(testing.allocator, output, .zig);
defer ast.deinit(testing.allocator);
try testing.expect(ast.errors.len == 0);
}
Validation Steps
-
Remove manual definitions from mock_test.zig:
- pub const Window = opaque {}; - pub const Rect = extern struct { ... }; - pub const FColor = extern struct { ... }; -
Import generated file directly:
const gpu = @import("../../zig-out/gpu_test.zig"); -
Run tests:
zig build test-mocks # Should still pass!
Success Metrics
✅ scanTypeReferences finds 30+ type references in SDL_gpu.h
✅ collectDefinedTypes tracks 169 defined types
✅ Missing types: Window, Rect, FColor, FlipMode, PropertiesID detected
✅ All 5 missing types extracted from dependency headers
✅ Generated code compiles without manual definitions
✅ All 11 tests pass
✅ Build time increase < 1 second
Rollout Plan
- Day 1: Implement Components 1-2 (type scanning/collecting)
- Day 2: Implement Components 3-4 (include parsing/type extraction)
- Day 3: Integrate into main, test with SDL_gpu.h
- Day 4: Refine, handle edge cases, update tests
- Day 5: Documentation, final validation
Risks & Mitigation
| Risk | Mitigation |
|---|---|
| Can't find header files | Require header directory as input |
| Type not in any header | Emit warning, generate placeholder |
| Parsing dependency fails | Catch error, continue with other headers |
| Performance (parsing multiple headers) | Cache parsed headers, parse once |
| Circular dependencies | Not an issue - all types in one file |
This plan is ready to implement. Each component is well-defined with clear inputs/outputs, error handling, and test cases.
File: src/type_collector.zig
const TypeCollector = struct {
defined_types: StringHashMap(void), // Types defined in primary header
referenced_types: StringHashMap(void), // Types used in signatures
pub fn collectFromDeclarations(decls: []Declaration) TypeCollector;
pub fn getMissingTypes() []const []const u8;
};
Tasks:
- Scan all declarations for type definitions (opaque, enum, struct, flags)
- Scan all function signatures for type references
- Return set difference: referenced - defined
Phase 2: Include Directive Parsing (1 hour)
File: src/patterns.zig (extend existing)
pub fn parseIncludes(source: []const u8) ![]const []const u8 {
// Find all #include <SDL3/header.h> directives
// Return list of header filenames
}
Tasks:
- Add regex/pattern for
#include <SDL3/...> - Extract header filename from directive
- Return list of included headers
Phase 3: Selective Type Extraction (2-3 hours)
File: src/type_extractor.zig
pub fn extractType(
allocator: Allocator,
header_source: []const u8,
type_name: []const u8, // e.g., "SDL_Rect"
) !?Declaration {
// Parse header looking for specific type
// Return the declaration if found
}
pub fn extractTypes(
allocator: Allocator,
header_paths: []const []const u8,
missing_types: []const []const u8,
) ![]Declaration {
// For each missing type:
// For each header:
// Try to extract the type
// If found, add to results
// Return all found declarations
}
Tasks:
- Reuse existing Scanner but filter by type name
- Handle opaque types:
typedef struct SDL_Type SDL_Type; - Handle structs:
typedef struct { ... } SDL_Type; - Handle enums:
typedef enum { ... } SDL_Type; - Handle simple typedefs:
typedef uint32_t SDL_Type;
Phase 4: Integration (1-2 hours)
File: src/parser.zig (modify main function)
pub fn main() !void {
// 1. Parse primary header
var scanner = Scanner.init(allocator, source);
const decls = try scanner.scan();
// 2. Collect missing types
const collector = TypeCollector.collectFromDeclarations(decls);
const missing_types = try collector.getMissingTypes(allocator);
// 3. Parse included headers for missing types
const includes = try parseIncludes(source);
const header_dir = getHeaderDirectory(header_path);
const dependency_decls = try extractTypes(
allocator,
header_dir,
includes,
missing_types
);
// 4. Generate code with dependencies appended
var all_decls = std.ArrayList(Declaration).init(allocator);
try all_decls.appendSlice(decls);
try all_decls.appendSlice(dependency_decls);
const output = try codegen.generate(allocator, all_decls.items);
// 5. Write output
try writeOutput(output_file, output);
}
Tasks:
- Wire together all components
- Handle header path resolution
- Add comment separators for dependencies
- Update error handling
Phase 5: Code Generation Enhancement (1 hour)
File: src/codegen.zig (modify)
Add dependency section:
fn generate() ![]const u8 {
try output.appendSlice("pub const c = @import(\"c.zig\").c;\n\n");
// Add comment if we have dependencies
if (has_dependency_decls) {
try output.appendSlice(
\\// Dependencies from included headers
\\// These types are referenced by the primary header
\\
);
}
// Generate all declarations (primary + dependencies)
for (decls) |decl| {
try generateDeclaration(decl);
}
}
Tasks:
- Add dependency comment section
- Mark which declarations are dependencies (optional)
- Ensure proper ordering (dependencies before usage)
Example Output
pub const c = @import("c.zig").c;
// Dependencies from included headers
// These types are referenced by SDL_gpu.h
// From SDL_rect.h
pub const Rect = extern struct {
x: i32,
y: i32,
w: i32,
h: i32,
};
// From SDL_pixels.h
pub const FColor = extern struct {
r: f32,
g: f32,
b: f32,
a: f32,
};
// From SDL_video.h
pub const Window = opaque {};
// From SDL_properties.h
pub const PropertiesID = u32;
// SDL_gpu.h declarations
pub const GPUDevice = opaque {
pub inline fn windowSupportsGPUSwapchainComposition(
gpudevice: *GPUDevice,
window: ?*Window, // ✅ Now defined!
swapchain_composition: GPUSwapchainComposition
) bool { ... }
};
pub const GPURenderPass = opaque {
pub inline fn setGPUScissor(
gpurenderpass: *GPURenderPass,
scissor: *const Rect // ✅ Now defined!
) void { ... }
};
Edge Cases
-
Type not found in any header
- Emit warning
- Generate placeholder:
pub const TypeName = opaque {};
-
Circular dependencies
- Not an issue - all types in one file
- Zig allows forward references
-
Multiple definitions
- Keep first definition found
- Zig will error if layouts differ (good!)
-
Typedef chains
typedef SDL_Type1 Type2;- Resolve transitively or use Zig's type alias
-
Complex types
- Function pointers: Skip for now, add TODO comment
- Unions: Skip for now, add TODO comment
- Nested structs: Should work fine
Testing Strategy
-
Unit tests for each component:
- TypeCollector: Test with known declarations
- parseIncludes: Test with sample headers
- extractType: Test finding types in headers
-
Integration test:
- Parse SDL_gpu.h
- Verify missing types are detected
- Verify dependencies are extracted
- Verify output compiles
-
Validation:
- Remove manual type definitions from mock_test.zig
- Import actual generated file
- All tests should still pass
Success Criteria
✅ Parser detects 5 missing types from SDL_gpu.h ✅ Parser extracts types from dependency headers ✅ Generated code compiles standalone ✅ All tests pass without manual type definitions ✅ Single output file contains all needed types
Time Estimate
- Phase 1 (Type Collection): 1-2 hours
- Phase 2 (Include Parsing): 1 hour
- Phase 3 (Type Extraction): 2-3 hours
- Phase 4 (Integration): 1-2 hours
- Phase 5 (Code Gen Enhancement): 1 hour
- Testing & Refinement: 2 hours
Total: 8-11 hours
Advantages of This Approach
- Simple: Single output file, no module management
- Fast: Only parse dependency headers when needed
- Minimal: Only extract required types
- Robust: Zig handles duplicate definitions
- Maintainable: Clear separation in output
Open Questions
-
Should we cache parsed dependency headers?
- Answer: Yes, parse once, extract many types
-
How to handle nested dependencies (Type A needs Type B)?
- Answer: Recursive extraction, track visited types
-
Should dependencies go at top or bottom of file?
- Answer: Top, before primary declarations use them
-
What about #define constants?
- Answer: Skip for now, out of scope
Next Steps
- Implement TypeCollector
- Implement include parsing
- Implement type extraction
- Wire together in main
- Test with SDL_gpu.h
- Update documentation
Ready to implement? This plan provides:
- Clear phases with time estimates
- Concrete code examples
- Handles edge cases
- Simple single-file output
- Full testing strategy