# AGENTS.md - Solutions to Common Issues in Zig Development This document captures the main issues encountered during the zargs implementation and their solutions. This is valuable for AI coding agents and developers working with Zig 0.15+. ## Table of Contents 1. [Zig 0.15 API Changes](#zig-015-api-changes) 2. [Comptime vs Runtime Issues](#comptime-vs-runtime-issues) 3. [Memory Management](#memory-management) 4. [Type System Challenges](#type-system-challenges) 5. [Build System Issues](#build-system-issues) 6. [Testing Strategies](#testing-strategies) --- ## Zig 0.15 API Changes ### Issue 1: Type Union Field Names Changed **Problem**: Code using `@typeInfo()` breaks with errors about union field names. ```zig // Zig 0.14 and earlier: if (info == .Bool) { ... } // Zig 0.15: if (info == .bool) { ... } // lowercase! ``` **Solution**: All `@typeInfo()` union fields are now lowercase: - `.Bool` → `.bool` - `.Int` → `.int` - `.Pointer` → `.pointer` - `.Enum` → `.@"enum"` - `.Optional` → `.optional` **How to Fix**: Search your codebase for patterns like `== .Bool` or `.Int` and convert to lowercase. --- ### Issue 2: Struct Field `default_value` → `default_value_ptr` **Problem**: `std.builtin.Type.StructField.default_value` doesn't exist. ```zig // Zig 0.14: if (field.default_value) |val| { ... } // Zig 0.15: if (field.default_value_ptr) |ptr| { ... } ``` **Solution**: Use `default_value_ptr` which is `?*const anyopaque`. Cast it to the field's type: ```zig if (field.default_value_ptr) |default_ptr| { const value_ptr: *const T = @ptrCast(@alignCast(default_ptr)); const value = value_ptr.*; // Use value... } ``` --- ### Issue 3: ArrayList API Changes **Problem**: `ArrayList(T).init()` doesn't exist, `deinit()` signature changed. **Solution**: Use `ArrayListUnmanaged` for better control: ```zig // OLD (doesn't work in 0.15): var list = std.ArrayList(T).init(allocator); list.deinit(); // NEW (Zig 0.15): var list = std.ArrayListUnmanaged(T){}; try list.append(allocator, item); list.deinit(allocator); ``` **Why**: `ArrayListUnmanaged` doesn't store the allocator, so `deinit()` needs it passed in. --- ### Issue 4: Module System Changes **Problem**: Direct file imports cause "file exists in multiple modules" errors. ```zig // DON'T DO THIS: const utils = @import("utils.zig"); // DO THIS: const utils = @import("utils"); ``` **Solution**: In `build.zig`, set up proper module dependencies: ```zig const utils_mod = b.addModule("utils", .{ .root_source_file = b.path("src/utils.zig"), ... }); const other_mod = b.addModule("other", .{ .root_source_file = b.path("src/other.zig"), .imports = &.{ .{ .name = "utils", .module = utils_mod }, }, }); ``` Then import by module name, not file path. --- ## Comptime vs Runtime Issues ### Issue 5: Returning Pointers to Comptime Locals **Problem**: Functions that return pointers to comptime local variables fail when called from runtime contexts. ```zig // BROKEN: pub fn toKebabCase(comptime name: []const u8) []const u8 { comptime { var result: [100]u8 = undefined; // ... fill result ... return result[0..len]; // ERROR: returning pointer to local! } } ``` **Error**: "function called at runtime cannot return value at comptime" **Root Cause**: Even though the function is `comptime`, if it's called from a runtime function (even a comptime parameter in a runtime function), Zig can't guarantee the returned pointer's lifetime. **Solution Options**: 1. **Inline the logic**: Don't return pointers, inline the computation: ```zig // In caller: inline for (fields) |field| { const field_name = field.name; // Already comptime // Use field_name directly } ``` 2. **Return arrays, not slices**: If size is comptime-known: ```zig pub fn toKebabCase(comptime name: []const u8) [computeLen(name)]u8 { // Return array by value, not pointer } ``` 3. **Use comptime string literals**: Store in the struct directly: ```zig const arg_name = if (user_meta.name) |custom| custom // This is a string literal else field.name; // This is also a string literal ``` **Workaround We Used**: Temporarily disabled kebab-case conversion and used `field.name` directly (which is always a comptime string literal). --- ### Issue 6: Comptime Arrays in Runtime Structures **Problem**: Storing comptime array slices in runtime-instantiated structs. ```zig pub fn extractAllFieldMetadata(comptime T: type) []const ArgumentMetadata { comptime { var metadata: [fields.len]ArgumentMetadata = undefined; // Fill metadata... return &metadata; // ERROR! } } ``` **Solution**: Don't return slices of comptime arrays. Instead: 1. **Loop inline at call site**: ```zig // Instead of: const all_meta = extractAllFieldMetadata(T); for (all_meta) |meta| { ... } // Do this: inline for (type_info.@"struct".fields) |field| { const meta = extractFieldMetadata(T, field); // Use meta immediately } ``` 2. **Copy into runtime storage**: If you must store, allocate and copy: ```zig const comptime_data = extractSomething(T); const runtime_copy = try allocator.dupe(T, comptime_data); ``` --- ## Memory Management ### Issue 7: StringHashMap Key Ownership **Problem**: Using temporary strings as HashMap keys causes dangling pointers. ```zig // BROKEN: const short_key = &[_]u8{short_char}; // Temporary! try self.arguments.put(short_key, metadata); // short_key is now dangling! ``` **Solution**: Allocate persistent keys: ```zig const short_key = try self.allocator.alloc(u8, 1); short_key[0] = short_char; try self.arguments.put(short_key, metadata); // short_key is now owned by the HashMap ``` **Don't Forget Cleanup**: ```zig pub fn deinit(self: *Self) void { var key_iter = self.map.keyIterator(); while (key_iter.next()) |key| { if (key.len == 1) { // Our allocated short keys self.allocator.free(key.*); } } self.map.deinit(); } ``` --- ### Issue 8: HashMap Value vs Pointer Storage **Problem**: Storing pointers to comptime data in HashMaps. ```zig // BROKEN: arguments: std.StringHashMap(*const ArgumentMetadata), const comptime_meta = extractMetadata(...); try arguments.put(name, &comptime_meta); // Pointer to comptime data! ``` **Solution**: Store values, not pointers: ```zig arguments: std.StringHashMap(ArgumentMetadata), // Value, not pointer const comptime_meta = extractMetadata(...); try arguments.put(name, comptime_meta); // Copy the value ``` **Accessing**: Use `getPtr()` to get a pointer to the stored value: ```zig pub fn getArgument(self: *const Self, name: []const u8) ?*const ArgumentMetadata { if (self.arguments.getPtr(name)) |ptr| { return ptr; } return null; } ``` --- ## Type System Challenges ### Issue 9: Checking for Optional Types **Problem**: Detecting if a type is optional at comptime. **Solution**: ```zig const is_optional = @typeInfo(T) == .optional; // To get the child type: const ActualType = if (@typeInfo(T) == .optional) @typeInfo(T).optional.child else T; ``` **Use Case**: Determining if an argument is required: ```zig .required = user_meta.required orelse !is_optional, ``` --- ### Issue 10: Enum Type Introspection **Problem**: Getting enum field names at comptime. **Solution**: ```zig const enum_info = @typeInfo(EnumType).@"enum"; for (enum_info.fields) |field| { const name: []const u8 = field.name; // name is a comptime string literal } ``` **Converting from string to enum**: ```zig inline for (enum_info.fields) |field| { if (std.mem.eql(u8, str, field.name)) { return @field(EnumType, field.name); } } ``` --- ### Issue 11: Type Matching for Collision Detection **Problem**: Checking if two fields have compatible types. **Solution**: Use the `ArgumentType` enum for normalized comparison: ```zig pub const ArgumentType = enum { bool, u8, u16, u32, u64, i8, i16, i32, i64, string, string_list, enum_type, }; // Extract type: const arg_type = ArgumentType.fromZigType(field.type); // Compare: if (existing.arg_type == new.arg_type) { // Compatible! } ``` This handles optionals automatically since `fromZigType` unwraps them. --- ## Build System Issues ### Issue 12: Module Dependency Cycles **Problem**: "file exists in multiple modules" errors. **Solution**: Create a clear dependency graph: ```zig // Base modules (no dependencies): const base_mod = b.addModule("base", .{ .root_source_file = b.path("src/base.zig"), }); // Dependent modules: const derived_mod = b.addModule("derived", .{ .root_source_file = b.path("src/derived.zig"), .imports = &.{ .{ .name = "base", .module = base_mod }, }, }); ``` **Rule**: Never create circular dependencies. If module A imports B, B cannot import A. --- ### Issue 13: Test Module Configuration **Problem**: Tests can't find imports. **Solution**: Set up test modules with all dependencies: ```zig const test_mod = b.createModule(.{ .root_source_file = b.path("tests/test_foo.zig"), .target = target, .optimize = optimize, .imports = &.{ .{ .name = "foo", .module = foo_mod }, .{ .name = "bar", .module = bar_mod }, // Include ALL transitive dependencies }, }); const tests = b.addTest(.{ .name = "foo-tests", .root_module = test_mod, }); ``` --- ## Testing Strategies ### Issue 14: Testing Comptime Functions **Problem**: Comptime functions can't be tested with runtime tests directly. **Solution**: Use comptime test blocks: ```zig test "comptime function" { const result = comptime myComptimeFunc("input"); try std.testing.expectEqualStrings("expected", result); } ``` Or embed comptime assertions in the source: ```zig // In source file: comptime { const result = toKebabCase("camelCase"); if (!std.mem.eql(u8, result, "camel-case")) { @compileError("toKebabCase failed"); } } ``` --- ### Issue 15: Memory Leak Detection in Tests **Problem**: Ensuring tests don't leak memory. **Solution**: Use `std.testing.allocator` and verify cleanup: ```zig test "no leaks" { var registry = Registry.init(std.testing.allocator); defer registry.deinit(); // Do stuff... // If deinit() doesn't free everything, test will fail } ``` The testing allocator tracks all allocations and will fail if any aren't freed. --- ## Best Practices Learned ### 1. Comptime String Management **Rule**: Comptime strings are fine as long as they're string literals or stored by value in comptime structures. **DO**: ```zig const name = field.name; // String literal const meta = ArgumentMetadata{ .arg_name = name, // Stores the pointer to literal }; ``` **DON'T**: ```zig const name = generateName(...); // Returns pointer to local const meta = ArgumentMetadata{ .arg_name = name, // Dangling pointer! }; ``` --- ### 2. Inline For Loops **Rule**: When iterating over comptime arrays from runtime contexts, use `inline for`: ```zig inline for (comptime_array) |item| { // This unrolls at compile time // Each iteration can use comptime values } ``` --- ### 3. Error Handling Patterns **Strategy**: Use error unions consistently: ```zig pub const Error = error{ ... }; pub fn function() Error!void { // Can return any error from Error set } // Caller: function() catch |err| { switch (err) { error.Specific => { ... }, else => { ... }, } }; ``` --- ### 4. Arena Allocator for Parsing **Pattern**: Use an arena for temporary parsing data: ```zig var arena = std.heap.ArenaAllocator.init(parent_allocator); defer arena.deinit(); const allocator = arena.allocator(); // All allocations freed at once when arena is deinit'd ``` --- ### 5. Type-Safe Unions **Pattern**: Use tagged unions for type-safe value storage: ```zig pub const Value = union(enum) { bool: bool, int: i64, string: []const u8, pub fn asBool(self: Value) bool { return switch (self) { .bool => |b| b, else => unreachable, }; } }; ``` --- ## Debugging Tips ### 1. Comptime Error Messages When you get cryptic comptime errors: - Look for "referenced by" chain - Start at the deepest call in the chain - Check if you're mixing comptime/runtime inappropriately ### 2. Type Info Inspection Debug type problems: ```zig const info = @typeInfo(T); std.debug.print("Type info: {}\n", .{info}); ``` ### 3. Build Cache Issues If build behavior is weird: ```bash rm -rf .zig-cache zig-out zig build ``` ### 4. Test Isolation Run single test: ```bash zig test src/file.zig --test-filter "test name" ``` --- ## Summary Checklist When implementing similar features: - [ ] Check for Zig 0.15 API changes (lowercase type names, default_value_ptr, etc.) - [ ] Avoid returning pointers to comptime locals - [ ] Use `inline for` when iterating comptime arrays from runtime contexts - [ ] Store values in HashMaps, not pointers to comptime data - [ ] Allocate HashMap keys that need to persist - [ ] Free allocated HashMap keys in deinit() - [ ] Use `ArrayListUnmanaged` and pass allocator to deinit() - [ ] Set up proper module dependencies in build.zig - [ ] Test with `std.testing.allocator` to catch leaks - [ ] Use arena allocators for temporary allocations --- ## Resources - [Zig 0.15 Release Notes](https://ziglang.org/download/0.15.0/release-notes.html) - [Zig Language Reference](https://ziglang.org/documentation/master/) - [Zig Build System Documentation](https://ziglang.org/learn/build-system/) --- **Document Version**: 1.0 **Last Updated**: 2026-01-22 **Zig Version**: 0.15.2