sdlparser-scrap/docs/DEVELOPMENT.md

635 lines
14 KiB
Markdown

# Development Guide
Guide for contributing to and extending the SDL3 header parser.
## Quick Start for Developers
```bash
# Clone and build
cd lib/sdl3/parser
zig build
# Run tests
zig build test
# Make changes
# ... edit src/*.zig ...
# Test your changes
zig build test
zig build run -- ../SDL/include/SDL3/SDL_gpu.h --output=test.zig
```
## Project Structure
```
parser/
├── src/
│ ├── parser.zig # Main entry point, CLI
│ ├── patterns.zig # Pattern matching & scanning
│ ├── types.zig # C to Zig type conversion
│ ├── naming.zig # Naming conventions
│ ├── codegen.zig # Zig code generation
│ ├── mock_codegen.zig # C mock generation
│ └── dependency_resolver.zig # Dependency analysis
├── test/
│ └── (test files)
├── docs/
│ └── (documentation)
└── build.zig
```
## Zig 0.15 API Changes - CRITICAL
**This project uses Zig 0.15**. Key API changes from 0.14:
### ArrayList Changes
**Old (0.14)**:
```zig
var list = std.ArrayList(T).init(allocator);
defer list.deinit();
try list.append(item);
```
**New (0.15)** - REQUIRED:
```zig
var list = std.ArrayList(T){};
defer list.deinit(allocator);
try list.append(allocator, item);
```
**Key Points**:
- Initialize with `{}` or `initCapacity()`
- All methods take allocator: `append(allocator, item)`
- Deinit takes allocator: `deinit(allocator)`
### AST Rendering
**Old**: `ast.render(allocator)`
**New**: `ast.renderAlloc(allocator)`
## Adding New Pattern Support
### Example: Adding Union Support
1. **Add to Declaration union** (patterns.zig):
```zig
pub const Declaration = union(enum) {
// ... existing variants
union_decl: UnionDecl, // NEW
};
pub const UnionDecl = struct {
name: []const u8,
fields: []FieldDecl,
doc_comment: ?[]const u8,
};
```
2. **Add scanner function** (patterns.zig):
```zig
fn scanUnion(self: *Scanner) !?UnionDecl {
// Pattern matching logic
// Return UnionDecl or null
}
```
3. **Add to scan chain** (patterns.zig):
```zig
if (try self.scanOpaque()) |opaque_decl| {
// ...
} else if (try self.scanUnion()) |union_decl| {
try decls.append(self.allocator, .{ .union_decl = union_decl });
} else if (try self.scanEnum()) |enum_decl| {
// ...
}
```
4. **Update cleanup code** (parser.zig):
```zig
defer {
for (decls) |decl| {
switch (decl) {
// ... existing cases
.union_decl => |u| {
allocator.free(u.name);
if (u.doc_comment) |doc| allocator.free(doc);
for (u.fields) |field| {
allocator.free(field.name);
allocator.free(field.type_name);
if (field.comment) |c| allocator.free(c);
}
allocator.free(u.fields);
},
}
}
}
```
5. **Update dependency resolver** (dependency_resolver.zig):
```zig
// In collectDefinedTypes:
.union_decl => |u| u.name,
// In cloneDeclaration:
.union_decl => |u| .{
.union_decl = .{
.name = try allocator.dupe(u8, u.name),
.fields = try cloneFields(allocator, u.fields),
.doc_comment = if (u.doc_comment) |doc|
try allocator.dupe(u8, doc) else null,
},
},
// In freeDeclaration:
.union_decl => |u| {
allocator.free(u.name);
if (u.doc_comment) |doc| allocator.free(doc);
for (u.fields) |field| {
allocator.free(field.name);
allocator.free(field.type_name);
if (field.comment) |c| allocator.free(c);
}
allocator.free(u.fields);
},
```
6. **Add code generator** (codegen.zig):
```zig
fn writeUnion(self: *CodeGen, union_decl: patterns.UnionDecl) !void {
const zig_name = naming.typeNameToZig(union_decl.name);
if (union_decl.doc_comment) |doc| {
try self.writeDocComment(doc);
}
try self.output.writer(self.allocator).print(
"pub const {s} = extern union {{\n",
.{zig_name}
);
for (union_decl.fields) |field| {
const zig_type = try types.convertType(field.type_name, self.allocator);
defer self.allocator.free(zig_type);
try self.output.writer(self.allocator).print(
" {s}: {s},\n",
.{field.name, zig_type}
);
}
try self.output.appendSlice(self.allocator, "};\n\n");
}
```
7. **Update writeDeclarations** (codegen.zig):
```zig
switch (decl) {
// ... existing cases
.union_decl => |union_decl| try self.writeUnion(union_decl),
}
```
8. **Add tests**:
```zig
test "parse union" {
const source =
\\typedef union SDL_Color {
\\ Uint32 rgba;
\\ struct { Uint8 r, g, b, a; };
\\} SDL_Color;
;
var scanner = patterns.Scanner.init(allocator, source);
const decls = try scanner.scan();
// ... test expectations
}
```
## Testing Guidelines
### Unit Tests
Place tests in `test/` or at bottom of source files:
```zig
test "descriptive test name" {
const allocator = std.testing.allocator;
// Setup
const source = "...";
var scanner = patterns.Scanner.init(allocator, source);
// Execute
const result = try scanner.scan();
defer allocator.free(result);
// Assert
try std.testing.expectEqual(expected, actual);
}
```
### Integration Tests
Test with real SDL headers:
```bash
zig build run -- ../SDL/include/SDL3/SDL_gpu.h --output=test.zig
zig ast-check test.zig
```
### Memory Testing
Always run tests with GPA to detect leaks:
```zig
test "my test" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Test code using allocator
}
```
## Code Style
### Naming
- **Functions**: camelCase (`parseStructField`)
- **Types**: PascalCase (`FieldDecl`)
- **Constants**: PascalCase (`Declaration`)
- **Variables**: camelCase (`decl_name`)
### Comments
Only comment code that needs clarification:
```zig
// Good: Explains WHY
// Must check flags before typedefs - pattern order matters
if (try self.scanFlagTypedef()) |flag_decl| { ... }
// Bad: Explains WHAT (obvious from code)
// Append to list
try list.append(allocator, item);
```
### Error Handling
Use graceful degradation:
```zig
// Good: Continue on error
const decl = extractType(source, name) catch |err| {
std.debug.print("Warning: {}\n", .{err});
continue;
};
// Bad: Fail immediately (unless truly fatal)
const decl = try extractType(source, name);
```
## Memory Management Rules
### Ownership
1. **Scanner owns strings** during parsing
2. **Caller owns result** of scan()
3. **HashMap owns keys** when they're duped
4. **Cloned declarations own strings** after cloning
### Cleanup Pattern
Always use defer for cleanup:
```zig
const decls = try scanner.scan();
defer {
for (decls) |decl| {
freeDeclDeep(allocator, decl);
}
allocator.free(decls);
}
```
### HashMap Keys
Must be owned (not slices into temporary data):
```zig
// Wrong:
try map.put(type_str, {}); // type_str might be freed!
// Right:
if (!map.contains(type_str)) {
const owned = try allocator.dupe(u8, type_str);
try map.put(owned, {});
}
```
## Common Patterns
### Pattern Matching
```zig
fn scanSomething(self: *Scanner) !?SomeDecl {
const start = self.pos;
const line = try self.readLine();
defer self.allocator.free(line);
// Check pattern
if (!std.mem.startsWith(u8, line, "expected_start")) {
self.pos = start; // Reset position
return null;
}
// Parse and return
return SomeDecl{ ... };
}
```
### String Building
```zig
var buf = std.ArrayList(u8){};
defer buf.deinit(allocator);
try buf.appendSlice(allocator, "pub const ");
try buf.appendSlice(allocator, name);
try buf.appendSlice(allocator, " = ");
return try buf.toOwnedSlice(allocator);
```
### HashMap Usage
```zig
var map = std.StringHashMap(void).init(allocator);
defer {
var it = map.keyIterator();
while (it.next()) |key| {
allocator.free(key.*); // Free owned keys
}
map.deinit();
}
// Add items
const owned_key = try allocator.dupe(u8, key);
try map.put(owned_key, {});
```
## Debugging Tips
### Print Debugging
```zig
std.debug.print("Debug: value = {s}\n", .{value});
std.debug.print("Type: {}\n", .{@TypeOf(variable)});
```
### Memory Leak Detection
Run with GPA and check output:
```bash
zig build run -- header.h 2>&1 | grep "memory address"
```
### AST Debugging
Check what Zig thinks is wrong:
```bash
zig ast-check generated.zig
```
## Performance Optimization
### Guidelines
1. **Avoid allocations in hot paths** - Use stack when possible
2. **Reuse buffers** - Clear and reuse instead of allocating new
3. **Early exit** - Return as soon as answer is known
4. **HashMap for lookups** - O(1) instead of O(n) searches
### Profiling
```bash
# Build with profiling
zig build -Drelease-safe
# Run with timing
time zig build run -- large_header.h --output=out.zig
```
## Contributing Workflow
1. **Create branch** from `dev/sdl3-parser`
2. **Make changes** in focused commits
3. **Run tests** - All must pass
4. **Update docs** if behavior changes
5. **Commit** with descriptive message
6. **Push** and create PR
### Commit Message Format
```
feat: Add union type support
Implements parsing and code generation for C union types.
- Added UnionDecl to Declaration union
- Implemented scanUnion() pattern matcher
- Added writeUnion() code generator
- Created comprehensive test suite (5 tests)
Results:
- Successfully parses SDL union types
- Generates proper extern unions
- All tests passing
Closes: #123
```
## Test-Driven Development
Recommended workflow:
1. **Write test first**:
```zig
test "parse union type" {
const source = "typedef union { int x; float y; } SDL_Union;";
var scanner = patterns.Scanner.init(allocator, source);
const decls = try scanner.scan();
try testing.expectEqual(@as(usize, 1), decls.len);
try testing.expect(decls[0] == .union_decl);
}
```
2. **Run test** (it will fail)
3. **Implement feature** until test passes
4. **Add more tests** for edge cases
5. **Refactor** if needed
## Architecture Decisions
### Why Single-File Output?
**Alternative**: Generate separate file per type
**Decision**: Single file with dependencies first
**Reasons**:
- Simpler for users (one import)
- Zig's structural typing handles it
- Type ordering guaranteed
- Less build system complexity
### Why On-Demand Resolution?
**Alternative**: Always parse all includes
**Decision**: Only parse when missing types detected
**Reasons**:
- Better performance
- Minimal overhead for self-contained headers
- Users see only relevant dependencies
### Why Conservative Error Handling?
**Alternative**: Fail on any error
**Decision**: Warn and continue
**Reasons**:
- Partial success is better than no success
- Users can manually fix issues
- Allows incremental improvement
## Extending the Parser
### Adding Type Conversions
Edit `src/types.zig`:
```zig
pub fn convertType(c_type: []const u8, allocator: Allocator) ![]const u8 {
// Check for your pattern first
if (std.mem.eql(u8, c_type, "MyCustomType")) {
return try allocator.dupe(u8, "MyZigType");
}
// Fall through to existing logic
// ...
}
```
### Adding Naming Rules
Edit `src/naming.zig`:
```zig
pub fn typeNameToZig(c_name: []const u8) []const u8 {
// Handle special cases
if (std.mem.eql(u8, c_name, "SDL_bool")) {
return "Bool"; // Custom mapping
}
// Default logic
return stripSDLPrefix(c_name);
}
```
### Adding Pattern Matchers
1. Implement `scan*()` function in `patterns.zig`
2. Add to scan chain with proper ordering
3. Update all switch statements
4. Add code generator
5. Write tests
See "Adding New Pattern Support" section above for full example.
## Common Issues When Developing
### Issue: ArrayList API Changed
**Error**: `error: no field named 'init' in struct 'ArrayList'`
**Solution**: Use Zig 0.15 API (see above)
### Issue: HashMap Key Lifetime
**Error**: Memory corruption or use-after-free
**Solution**: Always dupe keys before inserting:
```zig
const owned = try allocator.dupe(u8, key);
try map.put(owned, {});
```
### Issue: Pattern Matching Order
**Error**: Wrong scanner function matches
**Solution**: Order matters! More specific patterns first:
```zig
// Correct order:
if (try self.scanFlagTypedef()) { ... } // Specific
else if (try self.scanTypedef()) { ... } // General
// Wrong order:
if (try self.scanTypedef()) { ... } // Too general - catches flags!
else if (try self.scanFlagTypedef()) { ... } // Never reached
```
## Performance Considerations
### Current Performance
- Small headers: ~100ms
- Large headers (SDL_gpu.h): ~520ms
- Memory: ~2-5MB peak
### Bottlenecks
1. **Dependency extraction**: 300ms (58% of time)
- Parsing multiple headers
- Could cache parsed headers
2. **String allocations**: Many small allocations
- Could use arena allocator
- String interning would help
### Optimization Ideas
```zig
// Cache parsed headers
var header_cache = std.StringHashMap([]Declaration).init(allocator);
// Use arena for temporary allocations
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const temp_alloc = arena.allocator();
```
## Resources
### Zig Documentation
- [Zig Language Reference](https://ziglang.org/documentation/master/)
- [Zig Standard Library](https://ziglang.org/documentation/master/std/)
### SDL Documentation
- [SDL3 API](https://wiki.libsdl.org/SDL3/)
- [SDL3 Headers](https://github.com/libsdl-org/SDL)
### Project Documentation
- [Architecture](ARCHITECTURE.md) - How the parser works
- [Dependency Flow](DEPENDENCY_FLOW.md) - Detailed flow
- [Visual Flow](VISUAL_FLOW.md) - Diagrams
## Getting Help
- Check existing tests for examples
- Read [Architecture](ARCHITECTURE.md) for design
- See [Dependency Flow](DEPENDENCY_FLOW.md) for details
- Review git history for patterns
---
**Ready to contribute?** Start with the tests, understand the existing patterns, then extend!