# Zig 0.15 Skills and Gotchas This document tracks common issues, syntax changes, and gotchas encountered when working with Zig 0.15 during the SDL3 parser implementation. ## Work Summary Successfully implemented a fully functional SDL3 C header parser in Zig that converts SDL3 C headers into idiomatic Zig bindings. The parser uses a simplified text-matching approach (no full C parser) and successfully handles: - **Opaque types**: `typedef struct SDL_GPUDevice SDL_GPUDevice;` → `pub const GPUDevice = opaque {};` - **Enums**: C enums with value detection → Zig enums with camelCase values - **Structs**: C structs with field parsing → Zig extern structs with converted types - **Flags**: C typedef + #define flags → Zig packed structs - **Functions**: C function declarations → Zig inline wrapper functions with proper casts **Key Achievements**: - 4 core modules: patterns.zig (700+ lines), naming.zig (130+ lines), types.zig (88 lines), codegen.zig (339 lines) - All 14 tests passing - Zero memory leaks after fixes - Successfully parses test headers and generates valid Zig code - Proper handling of SDL naming conventions (SDL_GPUDevice → GPUDevice, SDL_CreateGPUDevice → createGPUDevice) **Major Bugs Fixed**: 1. Memory leaks from readLine() allocations - fixed with defer 2. Function name conversion bug (gPUSupportsShaderFormats) - fixed acronym lowercasing logic 3. Enum/struct parsing broken - fixed matchPrefix + readLine interaction 4. Brace characters appearing in parsed values - added brace line filtering ## Build System Changes ### root_module vs root_source_file **Issue**: In Zig 0.15, the build system API changed from `root_source_file` to `root_module`. **Old (pre-0.15)**: ```zig const parser_exe = b.addExecutable(.{ .name = "sdl-parser", .root_source_file = b.path("parser.zig"), .target = target, .optimize = optimize, }); ``` **New (0.15+)**: ```zig const parser_exe = b.addExecutable(.{ .name = "sdl-parser", .root_module = b.createModule(.{ .root_source_file = b.path("parser.zig"), .target = target, .optimize = optimize, }), }); ``` **Solution**: Use `root_module = b.createModule(.{...})` instead of passing fields directly. ## ArrayList API Changes ### Allocator Required for All Methods **Issue**: `ArrayList` methods now require passing the allocator explicitly, not just at initialization. **Old**: ```zig var list = std.ArrayList(u8).init(allocator); try list.append('x'); const slice = list.toOwnedSlice(); list.deinit(); ``` **New (0.15+)**: ```zig var list = try std.ArrayList(u8).initCapacity(allocator, initial_capacity); try list.append(allocator, 'x'); const slice = try list.toOwnedSlice(allocator); list.deinit(allocator); ``` **Solution**: Pass allocator to `append()`, `toOwnedSlice()`, `deinit()`, and use `initCapacity()` instead of `init()`. **Files affected**: - `naming.zig`: All ArrayList operations - `patterns.zig`: String building and result accumulation ## Reserved Keywords ### opaque is Reserved **Issue**: `opaque` is a reserved keyword in Zig and cannot be used as an identifier. **Error**: ```zig // ❌ This fails fn scanOpaque() !?OpaqueType { if (condition) |opaque| { // ERROR: 'opaque' is an identifier return opaque; } } // ❌ This also fails fn writeOpaque(self: *Self, opaque: OpaqueType) !void { ^~~~~~ // ERROR: expected '{', found ':' } // ❌ Even in tests test "opaque type" { const opaque = OpaqueType{...}; // ERROR: expected 'an identifier', found 'opaque' } ``` **Solution**: Use alternative names like `opaque_type`, `opaque_decl`, `opaque_val`: ```zig // ✅ Correct fn scanOpaque() !?OpaqueType { if (condition) |opaque_decl| { return opaque_decl; } } fn writeOpaque(self: *Self, opaque_type: OpaqueType) !void { // ... } test "opaque type" { const opaque_type = OpaqueType{...}; } ``` **Files affected**: - `patterns.zig:92`: Capture variable renamed to `opaque_decl` - `codegen.zig:44`: Capture variable renamed to `opaque_decl` - `codegen.zig:53`: Parameter renamed to `opaque_type` - `codegen.zig:247`: Test variable renamed to `opaque_type` ## Compiler Strictness ### Unused Variables are Errors **Issue**: Zig 0.15 treats unused variables as compilation errors, not warnings. **Error**: ```zig const value = getSomething(); // ERROR: unused local variable ``` **Solutions**: 1. Use the variable 2. Assign to `_` if intentionally unused: ```zig _ = getSomething(); ``` 3. Prefix with underscore for parameters: ```zig fn callback(_unused: u32) void {} ``` ### Error Unions Must be Handled **Issue**: Functions returning error unions must have errors explicitly handled. **Error**: ```zig const line = self.readLine(); // ERROR: error union not handled // readLine() returns ![]const u8 ``` **Solution**: Use `try` or explicit error handling: ```zig const line = try self.readLine(); // ✅ Correct // Or handle explicitly const line = self.readLine() catch |err| { return err; }; ``` **Files affected**: - `patterns.zig`: All `readLine()` calls needed `try` ## Memory Management ### Arena Allocator for Tests **Issue**: Using direct allocation in tests can cause memory leak errors that are hard to track. **Problem**: ```zig test "something" { var gpa = std.testing.allocator; const name = try gpa.alloc(u8, 10); // ... use name ... // If test fails before freeing, GPA reports leak } ``` **Solution**: Use arena allocator for test allocations: ```zig test "something" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const name = try allocator.alloc(u8, 10); // ... use name ... // Arena automatically frees everything on deinit } ``` **Files affected**: - `patterns.zig`: Test for opaque typedef uses arena ## Type System ### Const Pointer to Array vs Slice **Issue**: `&array` creates a const pointer to array, not a slice. **Error**: ```zig const decls = [_]Declaration{...}; function(&decls); // ERROR: expected '[]Declaration', found '*const [1]Declaration' ``` **Solution**: Use array slice syntax `&array` for mutable or `decls[0..]` for explicit slice: ```zig const decls = [_]Declaration{...}; function(decls[0..]); // ✅ Explicit slice // Or if function accepts const: function(&decls); // Works if function parameter is '[]const Declaration' ``` ## Parser Implementation Challenges ### matchPrefix() Consumes Input **Problem**: After calling `matchPrefix("typedef enum ")`, the scanner position has moved past the prefix. Calling `readLine()` immediately after will read from the new position, not from the start of the line. **Example**: ```zig // Source: "typedef enum SDL_GPUPrimitiveType {" if (self.matchPrefix("typedef enum ")) { // pos moves to after "typedef enum " const line = try self.readLine(); // Reads "SDL_GPUPrimitiveType {" // BUG: Trying to tokenize expecting "typedef enum name" var iter = std.mem.tokenizeScalar(u8, line, ' '); _ = iter.next(); // Expects "typedef" - NOT THERE _ = iter.next(); // Expects "enum" - NOT THERE const name = iter.next(); // Gets "SDL_GPUPrimitiveType" but after skipping non-existent tokens } ``` **Solution**: Don't use `readLine()` after `matchPrefix()`. Instead, scan forward to find landmarks (like opening brace), extract what you need from the source slice directly: ```zig if (self.matchPrefix("typedef enum ")) { // Find the opening brace const name_start = self.pos; while (self.pos < self.source.len and self.source[self.pos] != '{') { self.pos += 1; } // Extract name from the slice between matchPrefix and '{' const name_slice = std.mem.trim(u8, self.source[name_start..self.pos], " \t\n\r"); var iter = std.mem.tokenizeScalar(u8, name_slice, ' '); const name = iter.next() orelse return null; // Now we're positioned at '{', which is what readBracedBlock() expects const body = try self.readBracedBlock(); } ``` **Files affected**: - `patterns.zig:scanEnum()` - Fixed to scan for '{' instead of using readLine() - `patterns.zig:scanStruct()` - Applied same fix ### readBracedBlock() Returns Full Source Including Braces **Problem**: `readBracedBlock()` returns the entire source from current position to the end of the closing brace, including the braces themselves and any typedef name after the closing brace. **Example**: ```zig // Source at position: "{ VALUE1, VALUE2 } TypeName;" const body = try self.readBracedBlock(); // body = "{ VALUE1, VALUE2 } TypeName;" // ^ ^^^^^^^^^^ - includes closing brace and typedef name ``` When splitting by newlines and parsing, you get lines like: - `"{"` - `"VALUE1,"` - `"VALUE2"` - `"} TypeName;"` **Solution**: Filter out lines that start with braces when parsing the body: ```zig var lines = std.mem.splitScalar(u8, body, '\n'); while (lines.next()) |line| { const trimmed = std.mem.trim(u8, line, " \t\r"); if (trimmed.len == 0) continue; if (std.mem.startsWith(u8, trimmed, "//")) continue; if (std.mem.startsWith(u8, trimmed, "/*")) continue; if (std.mem.startsWith(u8, trimmed, "{")) continue; // ← Filter opening brace if (std.mem.startsWith(u8, trimmed, "}")) continue; // ← Filter closing brace + typedef name // Now parse the actual content if (try self.parseEnumValue(trimmed)) |value| { try values.append(self.allocator, value); } } ``` **Files affected**: - `patterns.zig:scanEnum()` - Added brace filtering for enum values - `patterns.zig:parseStructField()` - Added brace filtering for struct fields ### Memory Leaks from Temporary Allocations **Problem**: Functions that allocate memory (like `readLine()`) return owned slices that must be freed. In loops, forgetting to free these causes memory leaks. **Example**: ```zig // BUG: Memory leak while (!self.isAtEnd()) { const line = try self.readLine(); // Allocates try func_text.appendSlice(self.allocator, line); // line is never freed - LEAK! if (std.mem.indexOfScalar(u8, line, ';')) |_| break; } ``` **Solution**: Use `defer` to ensure allocation is freed even if loop breaks early: ```zig while (!self.isAtEnd()) { const line = try self.readLine(); defer self.allocator.free(line); // ← Always freed when scope exits try func_text.appendSlice(self.allocator, line); if (std.mem.indexOfScalar(u8, line, ';')) |_| break; } ``` **Files affected**: - `patterns.zig:scanFunction()` - Added defer for readLine() calls ### Leading Acronym Lowercasing in Function Names **Problem**: Simply lowercasing the first character of a function name doesn't work well with leading acronyms. **Example**: - `SDL_GPUSupportsShaderFormats` → strip "SDL_" → `GPUSupportsShaderFormats` - Lowercase first char → `gPUSupportsShaderFormats` ❌ (should be `gpuSupportsShaderFormats`) **Solution**: Lowercase the entire leading acronym (consecutive uppercase letters) until you hit a lowercase letter that starts a new word: ```zig pub fn functionNameToZig(c_name: []const u8, allocator: Allocator) ![]const u8 { const without_prefix = stripSDLPrefix(c_name); var result = try allocator.dupe(u8, without_prefix); // Lowercase leading acronyms (e.g., "GPUSupports" -> "gpuSupports") var i: usize = 0; while (i < result.len and std.ascii.isUpper(result[i])) : (i += 1) { // If we have at least 2 uppercase letters and the next char is lowercase, // we've found the end of the acronym (e.g., "GPUs" -> "gpu" + "Supports") if (i > 0 and i + 1 < result.len and std.ascii.isLower(result[i + 1])) { // Don't lowercase this last uppercase letter - it starts the next word break; } result[i] = std.ascii.toLower(result[i]); } return result; } ``` **Results**: - `GPUSupportsShaderFormats` → `gpuSupportsShaderFormats` ✓ - `CreateGPUDevice` → `createGPUDevice` ✓ - `GPUTextureFormat` → `gpuTextureFormat` ✓ **Files affected**: - `naming.zig:functionNameToZig()` - Implemented acronym lowercasing logic ## Best Practices 1. **Always handle error unions**: Use `try` or explicit `catch` 2. **Avoid reserved keywords**: Check keyword list before naming variables 3. **Use arena allocators in tests**: Simplifies cleanup and prevents leak errors 4. **Pass allocators explicitly**: Don't assume methods have access to allocator 5. **Prefer explicit slices**: Use `array[0..]` instead of relying on coercion 6. **Check build API docs**: Build system API changes between versions 7. **Use defer for cleanup**: Especially in loops where early breaks can skip cleanup 8. **Don't mix matchPrefix with readLine**: Scanner position management requires care 9. **Filter implementation details from parsed data**: Braces, keywords, etc. aren't part of semantic content ## Common Error Messages | Error Message | Likely Cause | Solution | |--------------|--------------|----------| | `expected 'an identifier', found 'opaque'` | Using reserved keyword | Rename variable | | `expected type '[]T', found '*const [N]T'` | Array vs slice mismatch | Use `array[0..]` | | `error union not handled` | Missing `try` or `catch` | Add error handling | | `unused local variable` | Variable declared but not used | Use it or assign to `_` | | `expected 2 arguments, found 1` | ArrayList API change | Pass allocator explicitly | | `root_source_file` field doesn't exist | Old build API | Use `root_module` | ### Const Arrays in Tests Must Be Mutable for Slicing **Issue**: When passing array slices to functions, the array must be declared with `var` not `const`, even if you're not modifying the array itself. **Error**: ```zig test "generate opaque type" { const decls = [_]Declaration{...}; const output = try CodeGen.generate(allocator, decls[0..]); // ERROR: expected type '[]Declaration', found '*const [1]Declaration' // ERROR: cast discards const qualifier } ``` **Solution**: Declare the array with `var`: ```zig test "generate opaque type" { var decls = [_]Declaration{...}; // ✅ Use var const output = try CodeGen.generate(allocator, decls[0..]); } ``` **Files affected**: - `codegen.zig`: All test arrays changed from `const` to `var` ## Standard Library API Changes ### ArrayList.writer() Requires Allocator **Issue**: The `writer()` method on ArrayList now requires an allocator parameter. **Error**: ```zig var list = std.ArrayList(u8).initCapacity(allocator, 256); try list.writer().print("Hello", .{}); // ERROR: member function expected 1 argument(s), found 0 ``` **Solution**: Pass the allocator to `writer()`: ```zig var list = std.ArrayList(u8).initCapacity(allocator, 256); try list.writer(allocator).print("Hello", .{}); // ✅ Pass allocator ``` **Files affected**: - `codegen.zig`: All `writer()` calls updated to pass allocator ### std.io.getStdOut() Moved **Issue**: `std.io.getStdOut()` no longer exists in Zig 0.15. **Error**: ```zig const stdout = std.io.getStdOut().writer(); // ERROR: root source file struct 'Io' has no member named 'getStdOut' ``` **Solution**: Use `std.posix.write()` with `std.posix.STDOUT_FILENO`: ```zig // Old way (doesn't work): const stdout = std.io.getStdOut().writer(); try stdout.writeAll(output); // New way (Zig 0.15): _ = try std.posix.write(std.posix.STDOUT_FILENO, output); ``` **Files affected**: - `parser.zig:105`: Changed to use `std.posix.write()` ## Type Inference Issues ### Comptime Type Inference with Runtime Values **Issue**: When using if-else chains with runtime string comparisons, the compiler may try to infer types as comptime when they should be runtime. **Error**: ```zig const type_bits = if (std.mem.eql(u8, underlying_type, "u8")) 8 else if (std.mem.eql(u8, underlying_type, "u16")) 16 else 32; // ERROR: value with comptime-only type 'comptime_int' depends on runtime control flow ``` **Solution**: Explicitly annotate the type: ```zig const type_bits: u32 = if (std.mem.eql(u8, underlying_type, "u8")) 8 else if (std.mem.eql(u8, underlying_type, "u16")) 16 else 32; // ✅ Explicit type annotation ``` **Files affected**: - `codegen.zig:148`: Added explicit `u32` type annotation ### Bit Shift Operand Type Mismatch **Issue**: Bit shift operations require the right operand to be exactly the right type for the shift amount. **Error**: ```zig var bit: u6 = 0; while (bit < 32) : (bit += 1) { if (val == (@as(u32, 1) << bit)) return bit; // ERROR: expected type 'u5', found 'u6' // NOTE: unsigned 5-bit int cannot represent all possible unsigned 6-bit values } ``` **Explanation**: Shifting a `u32` requires a shift amount of type `u5` (since 2^5 = 32 bits). A `u6` can represent values 0-63, but only 0-31 are valid shift amounts for `u32`. **Solution**: Cast the shift amount to the correct type: ```zig var bit: u6 = 0; while (bit < 32) : (bit += 1) { if (val == (@as(u32, 1) << @as(u5, @intCast(bit)))) return bit; // ✅ Cast to u5 } ``` **Files affected**: - `codegen.zig:238`: Added `@as(u5, @intCast(bit))` cast ## Complete List of Compilation Errors Encountered During the SDL3 parser implementation, we encountered the following compilation errors in order: 1. **`root_source_file` field doesn't exist in build.zig** - File: `build.zig` - Fix: Changed to `root_module = b.createModule(...)` 2. **`init` expects 2 arguments, found 1 (ArrayList API)** - Files: `naming.zig`, `patterns.zig` - Fix: Changed to `initCapacity(allocator, capacity)` and passed allocator to all methods 3. **`expected 'an identifier', found 'opaque'` (reserved keyword)** - Files: `patterns.zig:92`, `codegen.zig:44`, `codegen.zig:53`, `codegen.zig:247` - Fix: Renamed all `opaque` variables to `opaque_decl` or `opaque_type` 4. **Error union not handled for `readLine()`** - File: `patterns.zig` - Fix: Added `try` keyword before all `readLine()` calls 5. **Memory leak in test (GPA reported leak)** - File: `patterns.zig` test - Fix: Changed to use arena allocator 6. **`expected type '[]Declaration', found '*const [1]Declaration'`** - File: `codegen.zig` tests - Fix: Changed arrays from `const` to `var` and used `array[0..]` syntax 7. **`writer()` member function expected 1 argument(s), found 0** - File: `codegen.zig` (multiple locations) - Fix: Passed allocator to all `writer()` calls: `writer(allocator)` 8. **`value with comptime-only type 'comptime_int' depends on runtime control flow`** - File: `codegen.zig:148` - Fix: Added explicit type annotation: `const type_bits: u32 = ...` 9. **`expected type 'u5', found 'u6'` (bit shift operand)** - File: `codegen.zig:238` - Fix: Cast shift amount: `@as(u5, @intCast(bit))` 10. **`getCastType` expects 1 argument, found 2** - File: `codegen.zig:249` - Fix: Removed allocator parameter, `getCastType()` returns enum not string 11. **`std.io.getStdOut()` - no member named 'getStdOut'** - File: `parser.zig:105` - Fix: Changed to `std.posix.write(std.posix.STDOUT_FILENO, output)` ## Best Practices 1. **Always handle error unions**: Use `try` or explicit `catch` 2. **Avoid reserved keywords**: Check keyword list before naming variables 3. **Use arena allocators in tests**: Simplifies cleanup and prevents leak errors 4. **Pass allocators explicitly**: Don't assume methods have access to allocator 5. **Prefer explicit slices**: Use `array[0..]` instead of relying on coercion 6. **Check build API docs**: Build system API changes between versions 7. **Annotate types when using runtime conditions**: Avoid comptime inference issues 8. **Match bit shift operand types**: Use correct size for shift amounts (u5 for u32, etc.) 9. **Use `var` for arrays that need slicing**: Even if you don't modify the array itself ## Common Error Messages | Error Message | Likely Cause | Solution | |--------------|--------------|----------| | `expected 'an identifier', found 'opaque'` | Using reserved keyword | Rename variable | | `expected type '[]T', found '*const [N]T'` | Array vs slice mismatch | Use `array[0..]` | | `error union not handled` | Missing `try` or `catch` | Add error handling | | `unused local variable` | Variable declared but not used | Use it or assign to `_` | | `expected 2 arguments, found 1` | ArrayList API change | Pass allocator explicitly | | `root_source_file` field doesn't exist | Old build API | Use `root_module` | | `member function expected 1 argument(s), found 0` | Missing allocator for ArrayList methods | Pass allocator to method | | `no member named 'getStdOut'` | Moved/renamed std lib function | Use `std.posix.write()` | | `comptime-only type depends on runtime control flow` | Missing explicit type annotation | Add `: TypeName` annotation | | `expected type 'u5', found 'u6'` | Shift operand type mismatch | Cast to correct shift type | | `cast discards const qualifier` | Trying to get mutable slice from const array | Declare array with `var` | ## Resources - Zig 0.15 Release Notes: https://ziglang.org/download/0.15.0/release-notes.html - Build System API: https://ziglang.org/documentation/master/std/#std.Build - ArrayList API: https://ziglang.org/documentation/master/std/#std.ArrayList - Reserved Keywords: https://ziglang.org/documentation/master/#Keywords