608 lines
13 KiB
Markdown
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
|