zargs/AGENTS.md

608 lines
13 KiB
Markdown

# 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