git:20260113.222b7de to git:20260705.e9e4712

38 added, 388 removed. Audit A to A.

---
name: zig-best-practices
- description: Provides Zig patterns for type-first development with tagged unions, explicit error sets, comptime validation, and memory management. Must use when reading or writing Zig files.
+ description: Use when reading or writing Zig files (.zig, build.zig, build.zig.zon).
---
# Zig Best Practices
- ## Type-First Development
-
- Types define the contract before implementation. Follow this workflow:
-
- 1. **Define data structures** - structs, unions, and error sets first
- 2. **Define function signatures** - parameters, return types, and error unions
- 3. **Implement to satisfy types** - let the compiler guide completeness
- 4. **Validate at comptime** - catch invalid configurations during compilation
-
- ### Make Illegal States Unrepresentable
+ Follows type-first, functional, and error handling patterns from CLAUDE.md. This skill covers Zig-specific idioms only.
- Use Zig's type system to prevent invalid states at compile time.
+ ## Type System Patterns
- **Tagged unions for mutually exclusive states:**
+ **Tagged unions for mutually exclusive states** — prevents invalid combinations that a struct with multiple nullable fields would allow:
```zig
- // Good: only valid combinations possible
const RequestState = union(enum) {
idle,
loading,
success: []const u8,
failure: anyerror,
};
-
- fn handleState(state: RequestState) void {
- switch (state) {
- .idle => {},
- .loading => showSpinner(),
- .success => |data| render(data),
- .failure => |err| showError(err),
- }
- }
-
- // Bad: allows invalid combinations
- const RequestState = struct {
- loading: bool,
- data: ?[]const u8,
- err: ?anyerror,
- };
```
- **Explicit error sets for failure modes:**
+ **Explicit error sets** — documents exactly what can fail; `anyerror` hides failure modes:
```zig
- // Good: documents exactly what can fail
- const ParseError = error{
- InvalidSyntax,
- UnexpectedToken,
- EndOfInput,
- };
-
- fn parse(input: []const u8) ParseError!Ast {
- // implementation
- }
-
- // Bad: anyerror hides failure modes
- fn parse(input: []const u8) anyerror!Ast {
- // implementation
- }
+ const ParseError = error{ InvalidSyntax, UnexpectedToken, EndOfInput };
+ fn parse(input: []const u8) ParseError!Ast { ... }
```
- **Distinct types for domain concepts:**
+ **Distinct types for domain IDs** — compiler prevents mixing up different ID types:
```zig
- // Prevent mixing up IDs of different types
const UserId = enum(u64) { _ };
const OrderId = enum(u64) { _ };
-
- fn getUser(id: UserId) !User {
- // Compiler prevents passing OrderId here
- }
-
- fn createUserId(raw: u64) UserId {
- return @enumFromInt(raw);
- }
```
- **Comptime validation for invariants:**
+ **Comptime validation** — catch invalid configurations at compile time, not runtime:
```zig
fn Buffer(comptime size: usize) type {
- if (size == 0) {
- @compileError("buffer size must be greater than 0");
- }
- if (size > 1024 * 1024) {
- @compileError("buffer size exceeds 1MB limit");
- }
- return struct {
- data: [size]u8 = undefined,
- len: usize = 0,
- };
- }
- ```
-
- **Non-exhaustive enums for extensibility:**
- ```zig
- // External enum that may gain variants
- const Status = enum(u8) {
- active = 1,
- inactive = 2,
- pending = 3,
- _,
- };
-
- fn processStatus(status: Status) !void {
- switch (status) {
- .active => {},
- .inactive => {},
- .pending => {},
- _ => return error.UnknownStatus,
- }
+ if (size == 0) @compileError("buffer size must be greater than 0");
+ return struct { data: [size]u8 = undefined, len: usize = 0 };
}
```
- ## Module Structure
-
- Larger cohesive files are idiomatic in Zig. Keep related code together: tests alongside implementation, comptime generics at file scope, public/private controlled by `pub`. Split only when a file handles genuinely separate concerns. The standard library demonstrates this pattern with files like `std/mem.zig` containing 2000+ lines of cohesive memory operations.
-
- ## Instructions
-
- - Return errors with context using error unions (`!T`); every function returns a value or an error. Explicit error sets document failure modes.
- - Use `errdefer` for cleanup on error paths; use `defer` for unconditional cleanup. This prevents resource leaks without try-finally boilerplate.
- - Handle all branches in `switch` statements; include an `else` clause that returns an error or uses `unreachable` for truly impossible cases.
- - Pass allocators explicitly to functions requiring dynamic memory; prefer `std.testing.allocator` in tests for leak detection.
- - Prefer `const` over `var`; prefer slices over raw pointers for bounds safety. Immutability signals intent and enables optimizations.
- - Avoid `anytype`; prefer explicit `comptime T: type` parameters. Explicit types document intent and produce clearer error messages.
- - Use `std.log.scoped` for namespaced logging; define a module-level `log` constant for consistent scope across the file.
- - Add or update tests for new logic; use `std.testing.allocator` to catch memory leaks automatically.
-
- ## Examples
-
- Explicit failure for unimplemented logic:
- ```zig
- fn buildWidget(widget_type: []const u8) !Widget {
- return error.NotImplemented;
- }
- ```
+ ## Memory Management
- Propagate errors with try:
- ```zig
- fn readConfig(path: []const u8) !Config {
- const file = try std.fs.cwd().openFile(path, .{});
- defer file.close();
- const contents = try file.readToEndAlloc(allocator, max_size);
- return parseConfig(contents);
- }
- ```
+ - Pass allocators explicitly to every function that allocates; no global allocator state.
+ - Place `defer resource.deinit()` immediately after acquisition — keeps cleanup co-located with creation.
+ - Use `errdefer` for cleanup on error paths; `defer` for unconditional cleanup.
+ - Use arena allocators for batch/temporary work; they free everything at once.
+ - Use `std.testing.allocator` in tests — reports leaks with stack traces.
- Resource cleanup with errdefer:
```zig
fn createResource(allocator: std.mem.Allocator) !*Resource {
const resource = try allocator.create(Resource);
- errdefer allocator.destroy(resource);
-
+ errdefer allocator.destroy(resource); // runs only on error
resource.* = try initializeResource();
return resource;
}
```
- Exhaustive switch with explicit default:
- ```zig
- fn processStatus(status: Status) ![]const u8 {
- return switch (status) {
- .active => "processing",
- .inactive => "skipped",
- _ => error.UnhandledStatus,
- };
- }
- ```
-
- Testing with memory leak detection:
- ```zig
- const std = @import("std");
-
- test "widget creation" {
- const allocator = std.testing.allocator;
- var list: std.ArrayListUnmanaged(u32) = .empty;
- defer list.deinit(allocator);
-
- try list.append(allocator, 42);
- try std.testing.expectEqual(1, list.items.len);
- }
- ```
-
- ## Memory Management
-
- - Pass allocators explicitly; never use global state for allocation. Functions declare their allocation needs in parameters.
- - Use `defer` immediately after acquiring a resource. Place cleanup logic next to acquisition for clarity.
- - Prefer arena allocators for temporary allocations; they free everything at once when the arena is destroyed.
- - Use `std.testing.allocator` in tests; it reports leaks with stack traces showing allocation origins.
-
- ### Examples
-
- Allocator as explicit parameter:
- ```zig
- fn processData(allocator: std.mem.Allocator, input: []const u8) ![]u8 {
- const result = try allocator.alloc(u8, input.len * 2);
- errdefer allocator.free(result);
-
- // process input into result
- return result;
- }
- ```
-
- Arena allocator for batch operations:
- ```zig
- fn processBatch(items: []const Item) !void {
- var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
- defer arena.deinit();
- const allocator = arena.allocator();
-
- for (items) |item| {
- const processed = try processItem(allocator, item);
- try outputResult(processed);
- }
- // All allocations freed when arena deinits
- }
- ```
-
- ## Logging
-
- - Use `std.log.scoped` to create namespaced loggers; each module should define its own scoped logger for filtering.
- - Define a module-level `const log` at the top of the file; use it consistently throughout the module.
- - Use appropriate log levels: `err` for failures, `warn` for suspicious conditions, `info` for state changes, `debug` for tracing.
-
- ### Examples
-
- Scoped logger for a module:
- ```zig
- const std = @import("std");
- const log = std.log.scoped(.widgets);
-
- pub fn createWidget(name: []const u8) !Widget {
- log.debug("creating widget: {s}", .{name});
- const widget = try allocateWidget(name);
- log.debug("created widget id={d}", .{widget.id});
- return widget;
- }
-
- pub fn deleteWidget(id: u32) void {
- log.info("deleting widget id={d}", .{id});
- // cleanup
- }
- ```
-
- Multiple scopes in a codebase:
- ```zig
- // In src/db.zig
- const log = std.log.scoped(.db);
-
- // In src/http.zig
- const log = std.log.scoped(.http);
-
- // In src/auth.zig
- const log = std.log.scoped(.auth);
- ```
-
- ## Comptime Patterns
-
- - Use `comptime` parameters for generic functions; type information is available at compile time with zero runtime cost.
- - Prefer compile-time validation over runtime checks when possible. Catch errors during compilation rather than in production.
- - Use `@compileError` for invalid configurations that should fail the build.
-
- ### Examples
-
- Generic function with comptime type:
- ```zig
- fn max(comptime T: type, a: T, b: T) T {
- return if (a > b) a else b;
- }
- ```
-
- Compile-time validation:
- ```zig
- fn createBuffer(comptime size: usize) [size]u8 {
- if (size == 0) {
- @compileError("buffer size must be greater than 0");
- }
- return [_]u8{0} ** size;
- }
- ```
-
- ## Avoiding anytype
-
- - Prefer `comptime T: type` over `anytype`; explicit type parameters document expected constraints and produce clearer errors.
- - Use `anytype` only when the function genuinely accepts any type (like `std.debug.print`) or for callbacks/closures.
- - When using `anytype`, add a doc comment describing the expected interface or constraints.
-
- ### Examples
-
- Prefer explicit comptime type (good):
- ```zig
- fn sum(comptime T: type, items: []const T) T {
- var total: T = 0;
- for (items) |item| {
- total += item;
- }
- return total;
- }
- ```
-
- Avoid anytype when type is known (bad):
- ```zig
- // Unclear what types are valid; error messages will be confusing
- fn sum(items: anytype) @TypeOf(items[0]) {
- // ...
- }
- ```
-
- Acceptable anytype for callbacks:
- ```zig
- /// Calls `callback` for each item. Callback must accept (T) and return void.
- fn forEach(comptime T: type, items: []const T, callback: anytype) void {
- for (items) |item| {
- callback(item);
- }
- }
- ```
-
- Using @TypeOf when anytype is necessary:
- ```zig
- fn debugPrint(value: anytype) void {
- const T = @TypeOf(value);
- if (@typeInfo(T) == .Pointer) {
- std.debug.print("ptr: {*}\n", .{value});
- } else {
- std.debug.print("val: {}\n", .{value});
- }
- }
- ```
-
- ## Error Handling Patterns
-
- - Define specific error sets for functions; avoid `anyerror` when possible. Specific errors document failure modes.
- - Use `catch` with a block for error recovery or logging; use `catch unreachable` only when errors are truly impossible.
- - Merge error sets with `||` when combining operations that can fail in different ways.
-
- ### Examples
-
- Specific error set:
- ```zig
- const ConfigError = error{
- FileNotFound,
- ParseError,
- InvalidFormat,
- };
-
- fn loadConfig(path: []const u8) ConfigError!Config {
- // implementation
- }
- ```
-
- Error handling with catch block:
- ```zig
- const value = operation() catch |err| {
- std.log.err("operation failed: {}", .{err});
- return error.OperationFailed;
- };
- ```
-
- ## Configuration
-
- - Load config from environment variables at startup; validate required values before use. Missing config should cause a clean exit with a descriptive message.
- - Define a Config struct as single source of truth; avoid `std.posix.getenv` scattered throughout code.
- - Use sensible defaults for development; require explicit values for production secrets.
-
- ### Examples
-
- Typed config struct:
- ```zig
- const std = @import("std");
-
- pub const Config = struct {
- port: u16,
- database_url: []const u8,
- api_key: []const u8,
- env: []const u8,
- };
-
- pub fn loadConfig() !Config {
- const db_url = std.posix.getenv("DATABASE_URL") orelse
- return error.MissingDatabaseUrl;
- const api_key = std.posix.getenv("API_KEY") orelse
- return error.MissingApiKey;
- const port_str = std.posix.getenv("PORT") orelse "3000";
- const port = std.fmt.parseInt(u16, port_str, 10) catch
- return error.InvalidPort;
-
- return .{
- .port = port,
- .database_url = db_url,
- .api_key = api_key,
- .env = std.posix.getenv("ENV") orelse "development",
- };
- }
- ```
+ ## Key Conventions
- ## Optionals
+ - Prefer `const` over `var`; prefer slices over raw pointers.
+ - Prefer `comptime T: type` over `anytype`; explicit types produce clearer errors. Use `anytype` only for genuinely polymorphic cases (callbacks, `std.debug.print`-style).
+ - Exhaustive `switch`: include an `else` returning an error or `unreachable` for truly impossible cases.
+ - Use `std.log.scoped(.module_name)` for namespaced logging; define a module-level `const log` constant.
+ - Larger cohesive files are idiomatic — tests alongside implementation, comptime generics at file scope.
- - Use `orelse` to provide default values for optionals; use `.?` only when null is a program error.
- - Prefer `if (optional) |value|` pattern for safe unwrapping with access to the value.
+ ## Advanced Topics
- ### Examples
+ - **Generic containers** (queues, stacks, trees): See [GENERICS.md](GENERICS.md)
+ - **C library interop** (raylib, SDL, curl): See [C-INTEROP.md](C-INTEROP.md)
+ - **Debugging memory leaks** (GPA, stack traces): See [DEBUGGING.md](DEBUGGING.md)
- Safe optional handling:
- ```zig
- fn findWidget(id: u32) ?*Widget {
- // lookup implementation
- }
+ ## Tooling
- fn processWidget(id: u32) !void {
- const widget = findWidget(id) orelse return error.WidgetNotFound;
- try widget.process();
- }
+ **zigdoc** — browse std library and dependency docs:
+ ```bash
+ zigdoc std.mem.Allocator # std lib symbol
+ zigdoc vaxis.Window # project dependency
+ zigdoc @init # create AGENTS.md with API patterns
```
- Optional with if unwrapping:
- ```zig
- if (maybeValue) |value| {
- try processValue(value);
- } else {
- std.log.warn("no value present", .{});
- }
+ **ziglint** — static analysis with `.ziglint.zon` config:
+ ```bash
+ ziglint # lint current directory
+ ziglint --ignore Z001 # suppress specific rule
```
- ## Advanced Topics
-
- Reference these guides for specialized patterns:
-
- - **Building custom containers** (queues, stacks, trees): See [GENERICS.md](GENERICS.md)
- - **Interfacing with C libraries** (raylib, SDL, curl, system APIs): See [C-INTEROP.md](C-INTEROP.md)
- - **Debugging memory leaks** (GPA, stack traces): See [DEBUGGING.md](DEBUGGING.md)
-
## References
- Language Reference: https://ziglang.org/documentation/0.15.2/
- Standard Library: https://ziglang.org/documentation/0.15.2/std/
- - Code Samples: https://ziglang.org/learn/samples/
- Zig Guide: https://zig.guide/