home / skills / tkersey / dotfiles / zig

zig skill

/codex/skills/zig

This skill helps you edit Zig code, validate builds, and fuzz tests to ensure correctness and performance across compiles.

npx playbooks add skill tkersey/dotfiles --skill zig

Review the files below or copy the command above to add this skill to your agents.

Files (7)
SKILL.md
28.4 KB
---
name: zig
description: "Use when requests involve Zig code or toolchain work: editing `.zig`, changing `build.zig`/`build.zig.zon`, running or fixing `zig build|test|run|fmt|fetch`, debugging compile/runtime/test failures, comptime/reflection/codegen, allocator ownership, SIMD (`std.simd`/`@Vector`), threads (`std.Thread`/`Thread.Pool`), cross-compilation, zero-copy parsing, or C interop (`@cImport`). Enforce correctness-first validation with tests, `std.testing.fuzz`, and allocation-failure checks."
---

# Zig

## When to use
- Editing `.zig` files.
- Modifying `build.zig` or `build.zig.zon`.
- Zig builds/tests, dependencies, cross-compilation.
- Any Zig work requires fuzz testing (coverage-guided or fuzz-style).
- Performance tuning: SIMD (`std.simd` / `@Vector`) and threading (`std.Thread.Pool`).
- Comptime, reflection, codegen.
- Allocators, ownership, zero-copy parsing.
- C interop.

## Baseline (required)
- Zig 0.15.2.
- Integrated fuzzer is the default: `std.testing.fuzz` + `zig build test --fuzz`.
- No compatibility work for older Zig unless explicitly requested.

## Quick start
```bash
# Toolchain (required)
zig version  # must be 0.15.2

# Initialize (creates build.zig + src/main.zig)
zig init
# or (smaller template)
zig init --minimal
# NOTE: --minimal does NOT add a `test` build step; `zig build test` / `--fuzz`
# will fail unless you add a test step to build.zig.

# Format
zig fmt src/main.zig

# Build/run/test (build.zig present)
zig build
zig build run
zig build test

# Fuzz (integrated fuzzer)
# Requires a `test` step in build.zig (not present in --minimal template).
zig build test --fuzz

# Single-file test/run
zig test src/main.zig
zig run src/main.zig

# Trigger audit (session-level proxy via seq)
python3 codex/skills/zig/scripts/zig_trigger_audit.py --root ~/.codex/sessions
python3 codex/skills/zig/scripts/zig_trigger_audit.py --root ~/.codex/sessions --since 2026-02-06T00:00:00Z
```

## Workflow (correctness -> speed)
- State the contract: input domain, outputs, invariants, error model, complexity target.
- Build a reference implementation (simple > fast) and keep it in-tree for diffing.
- Unit tests: edge cases + regressions.
- Differential fuzz: compare optimized vs reference in Debug/ReleaseSafe.
- Optimize in order: algorithm -> data layout -> SIMD -> threads -> micro.
- Re-run fuzz/tests after every optimization; benchmark separately in ReleaseFast.

## Correctness mandate (non-negotiable)
- Every Zig change earns at least one correctness signal.
- For parsing/arith/memory/safety-sensitive code, that signal is fuzzing.
- Prefer differential fuzzing (optimized vs reference) so behavior is proven, not inferred.
- Default harness: `std.testing.fuzz` + `zig build test --fuzz` (Zig 0.15.2 baseline).
- Time-agnostic: no prescribed fuzz duration; run it as long as practical and always persist findings.
- Run fuzz in `Debug`/`ReleaseSafe` so safety checks stay on; benchmark separately in `ReleaseFast`.
- Allocator-using code also runs `std.testing.checkAllAllocationFailures`.
- If fuzzing cannot run locally (e.g., macOS `InvalidElfMagic` crash), state why and add a
  follow-up (seed corpus + repro test); run fuzz in Linux/CI or external harness.

## Performance quick start (host CPU)
```bash
# High-performance build for local benchmarking
zig build-exe -O ReleaseFast -mcpu=native -fstrip src/main.zig

# Emit assembly / optimized IR for inspection
zig build-exe -O ReleaseFast -mcpu=native -femit-asm src/main.zig
zig build-exe -O ReleaseFast -mcpu=native -femit-llvm-ir src/main.zig  # requires LLVM extensions

# Build.zig projects (when using b.standardTargetOptions / standardOptimizeOption)
zig build -Doptimize=ReleaseFast -Dtarget=native -Dcpu=native
```

## Common commands
```bash
# Release
zig build -Doptimize=ReleaseFast

# Release + LTO (requires LLVM extensions)
zig build-exe -O ReleaseFast -mcpu=native -flto -fstrip src/main.zig

# Cross-compile
zig build -Dtarget=x86_64-linux
zig build -Dtarget=aarch64-macos

# Clean artifacts
rm -rf zig-out zig-cache
```

## Optimization stance (for generated code)
- Prefer algorithmic wins first; then data layout; then SIMD; then threads; then micro-tuning.
- Keep hot loops allocation-free; treat allocations as a correctness smell in kernels.
- Prefer contiguous slices and SoA layouts; avoid pointer chasing in the hot path.
- Avoid false sharing: make per-thread outputs cache-line separated (e.g. `align(std.atomic.cache_line)`).
- Help the optimizer: branchless vector loops, `@branchHint(.likely/.unlikely)`, and simple control flow.
- Keep fast paths portable: `std.simd.suggestVectorLength(T)` + scalar fallback; thread-pool usage already degrades on `builtin.single_threaded`.

## SIMD / vectorization playbook
Principles:
- Use explicit vectors when you need guaranteed SIMD (`@Vector`); rely on auto-vectorization only as a bonus.
- Derive lane count from `std.simd.suggestVectorLength(T)` so the same code scales across targets.
- Keep vector loops straight-line: no function pointers, no complex branching, no hidden allocations.
- Handle tails (remainder elements) with a scalar loop.
- Alignment matters on some targets (notably ARM); when tuning, consider a scalar prologue until aligned to the block size.

### SIMD template: reduce a slice
```zig
const std = @import("std");

pub fn sumF32(xs: []const f32) f32 {
    if (xs.len == 0) return 0;

    if (!@inComptime()) if (std.simd.suggestVectorLength(f32)) |lanes| {
        const V = @Vector(lanes, f32);

        var i: usize = 0;
        var acc: V = @splat(0);

        while (i + lanes <= xs.len) : (i += lanes) {
            const v: V = xs[i..][0..lanes].*;
            acc += v;
        }

        var total: f32 = @reduce(.Add, acc);
        while (i < xs.len) : (i += 1) total += xs[i];
        return total;
    }

    var total: f32 = 0;
    for (xs) |x| total += x;
    return total;
}
```

### SIMD scanning pattern (mask + reduce)
- Compare a vector against a scalar mask: `matches = block == @as(Block, @splat(value))`.
- Detect any matches: `if (@reduce(.Or, matches)) { ... }`.
- Find the first match index: `std.simd.firstTrue(matches).?`.

### SIMD delimiter bitmask pattern (CSV-class scanners)
Use a bitmask when you need all match positions in a block (delimiter, quote, CR, LF), not just
the first hit.

```zig
const std = @import("std");

fn delimMask(block: @Vector(16, u8), delim: u8, quote: u8, cr: u8, lf: u8) u16 {
    const matches: @Vector(16, bool) =
        (block == @as(@Vector(16, u8), @splat(delim))) or
        (block == @as(@Vector(16, u8), @splat(quote))) or
        (block == @as(@Vector(16, u8), @splat(cr))) or
        (block == @as(@Vector(16, u8), @splat(lf)));
    return @bitCast(matches);
}

fn walkMatches(mask0: u16) void {
    var mask = mask0;
    while (mask != 0) {
        const idx = @ctz(mask); // 0-based lane index
        _ = idx;
        mask &= mask - 1; // clear lowest set bit
    }
}
```

- Validate lane mapping with a unit test: after `@bitCast`, LSB corresponds to lane/index `0`.

### Loop shaping tips (stdlib-proven)
- Unroll short inner loops with `inline for` to cut bounds checks (see `std.mem.indexOfScalarPos`).
- Use `std.simd.suggestVectorLength(T)` to match stdlib’s preferred alignment and vector width.
- Guard vector paths with `!@inComptime()` and `!std.debug.inValgrind()` when doing anything tricky.

## Threads / parallelism playbook
Principles:
- Only thread if you can amortize scheduling + cache effects (tiny slices usually lose).
- Partition work by contiguous ranges; avoid shared writes and shared locks in the hot path.
- Use a thread pool (`std.Thread.Pool`) + wait group (`std.Thread.WaitGroup`), not "spawn a thread per task".
- Make tasks coarse: ~`cpu_count` to ~`8*cpu_count` tasks, each doing a SIMD inner loop.
- Reduce results at the end; avoid atomics unless you truly need streaming aggregation.

### Thread pool template (data-parallel)
```zig
const std = @import("std");
const builtin = @import("builtin");

fn sumChunk(xs: []const f32, out: *f32) void {
    // Each task uses the SIMD kernel.
    out.* = sumF32(xs);
}

pub fn sumParallel(xs: []const f32) !f32 {
    if (xs.len == 0) return 0;

    // For throughput-oriented programs, prefer std.heap.smp_allocator in ReleaseFast.
    // smp_allocator is unavailable when compiled with -fsingle-threaded.
    const alloc = if (builtin.single_threaded) std.heap.page_allocator else std.heap.smp_allocator;

    var pool: std.Thread.Pool = undefined;
    try pool.init(.{ .allocator = alloc });
    defer pool.deinit();

    const cpu_count = @max(1, std.Thread.getCpuCount() catch 1);
    const task_count = @min(cpu_count * 4, xs.len);
    const chunk_len = (xs.len + task_count - 1) / task_count;

    var partials = try alloc.alloc(f32, task_count);
    defer alloc.free(partials);

    var wg: std.Thread.WaitGroup = .{};

    for (0..task_count) |t| {
        const start = t * chunk_len;
        const end = @min(xs.len, start + chunk_len);
        pool.spawnWg(&wg, sumChunk, .{ xs[start..end], &partials[t] });
    }

    // Let the calling thread help execute queued work.
    pool.waitAndWork(&wg);

    var total: f32 = 0;
    for (partials) |p| total += p;
    return total;
}
```

### Per-thread scratch (no allocator contention)
- Initialize the pool with `.track_ids = true`.
- Use `pool.spawnWgId(&wg, func, args)`; `func` receives `id: usize` first.
- Keep `scratch[id]` aligned to `std.atomic.cache_line` to prevent false sharing.

```zig
const std = @import("std");

const Scratch = struct {
    _: void align(std.atomic.cache_line) = {},
    tmp: [4096]u8 = undefined,
};

fn work(id: usize, input: []const u8, scratch: []Scratch) void {
    // Stable per-thread slot; no locks, no false sharing.
    const buf = scratch[id].tmp[0..];
    _ = buf;
    _ = input;
}

pub fn runParallel(input: []const u8, allocator: std.mem.Allocator) !void {
    var pool: std.Thread.Pool = undefined;
    try pool.init(.{ .allocator = allocator, .track_ids = true });
    defer pool.deinit();

    const scratch = try allocator.alloc(Scratch, pool.getIdCount());
    defer allocator.free(scratch);

    var wg: std.Thread.WaitGroup = .{};
    pool.spawnWgId(&wg, work, .{ input, scratch });
    pool.waitAndWork(&wg);
}
```

## Comptime meta-programming (Zig 0.15.2)
Principles:
- Use comptime for specialization and validation; measure compile time like runtime.
- Prefer data over codegen; generate code only when it unlocks optimization.
- Make illegal states unrepresentable with `@compileError` at the boundary.

Core tools:
- Type reflection: `@typeInfo`, `@Type`, `@TypeOf`, `@typeName`.
- Namespaces/fields: `@hasDecl`, `@field`, `@FieldType`, `std.meta.fields`, `std.meta.declarations`.
- Layout + ABI: `@sizeOf`, `@alignOf`, `@bitSizeOf`, `@offsetOf`, `@fieldParentPtr`.
- Controlled unrolling: `inline for`, `inline while`, `comptime if`.
- Diagnostics: `@compileError`, `@compileLog`.
- Cost control: `@setEvalBranchQuota` (local, justified), `--time-report`.

Common patterns:
- Traits: assert required decls/methods at compile time.
- Field-wise derivations: generate `eql`/`hash`/`format`/`serialize` by iterating fields.
- Static tables: small `std.StaticStringMap.initComptime`; large enums prefer `inline for` scans (see `std.meta.stringToEnum`).
- Kernel factories: `fn Kernel(comptime lanes: usize, comptime unroll: usize) type { ... }` + `std.simd.suggestVectorLength`.

Unfair toolbox (stdlib-proven):
- `std.meta.eql`: deep-ish equality for containers (pointers are not followed).
- `std.meta.hasUniqueRepresentation`: gate "memcmp-style" fast paths.
- `std.meta.FieldEnum` / `std.meta.DeclEnum`: turn fields/decls into enums for ergonomic switches.
- `std.meta.Tag` / `std.meta.activeTag`: read tags of tagged unions.
- `std.meta.fields` / `std.meta.declarations`: one-liners for reflection without raw `@typeInfo` plumbing.

### Trait check template
```zig
const std = @import("std");

fn assertHasRead(comptime T: type) void {
    if (!std.meta.hasMethod(T, "read")) {
        @compileError(@typeName(T) ++ " must implement read()");
    }
}
```

### Field-wise derivation template (struct)
```zig
const std = @import("std");

fn eqlStruct(a: anytype, b: @TypeOf(a)) bool {
    const T = @TypeOf(a);
    if (@typeInfo(T) != .@"struct") @compileError("eqlStruct expects a struct");

    inline for (std.meta.fields(T)) |f| {
        if (!std.meta.eql(@field(a, f.name), @field(b, f.name))) return false;
    }
    return true;
}
```

### Layout assertions (ABI lock)
```zig
comptime {
    const Header = extern struct {
        magic: u32,
        version: u16,
        flags: u16,
        len: u32,
        _pad: u32,
    };

    if (@sizeOf(Header) != 16) @compileError("Header ABI: size");
    if (@alignOf(Header) != 4) @compileError("Header ABI: align");
    if (@offsetOf(Header, "magic") != 0) @compileError("Header ABI: magic offset");
    if (@offsetOf(Header, "len") != 8) @compileError("Header ABI: len offset");
}
```

### Union visitor (inline switch)
```zig
fn visit(u: anytype) void {
    switch (u) {
        inline else => |payload, tag| {
            _ = payload;
            _ = tag; // comptime-known tag
        },
    }
}
```

### Type-shape dispatcher (derive-anything)
Use this to write one derivation pipeline that supports structs/unions/enums/pointers/arrays/etc.
Keep the return type uniform (`R`) so call sites stay simple.

Implementation + tests: `codex/skills/zig/references/type_switch.zig`.
Validate: `zig test codex/skills/zig/references/type_switch.zig`

### `@Type` builder (surgical)
Reach for `@Type` when you truly need to manufacture a new type from an input type.
This is sharp: prefer `std.meta.*` when it can express the same intent.

Example: build a "patch" type where every runtime field is `?T` defaulting to `null`.

Implementation + tests: `codex/skills/zig/references/partial_type.zig`.
Validate: `zig test codex/skills/zig/references/partial_type.zig`

### Derive pipeline (walk + policies; truly unfair)
One traversal emits semantic events; policies decide what to do (hash, format, serialize, stats).
Traversal owns ordering + budgets; policies own semantics.

Implementation + tests: `codex/skills/zig/references/derive_walk_policy.zig`.
Validate: `zig test codex/skills/zig/references/derive_walk_policy.zig`
Note: formatting helpers take a writer pointer (e.g. `&w`) so state is preserved.

### Fast path when representation is unique
```zig
const std = @import("std");

fn eqlFast(a: anytype, b: @TypeOf(a)) bool {
    const T = @TypeOf(a);
    if (comptime std.meta.hasUniqueRepresentation(T)) {
        return std.mem.eql(u8, std.mem.asBytes(&a), std.mem.asBytes(&b));
    }
    return std.meta.eql(a, b);
}
```

### Compile-time cost guardrails
- Avoid combinatorial specialization: keep the knob surface small and explicit.
- Avoid huge comptime maps for large domains; prefer `inline for` scans.
- If you must raise the branch quota, do it in the smallest loop that needs it.
- Prefer `std.meta.*` helpers over handwritten `@typeInfo` plumbing (less code, fewer bugs).

### Comptime example
```zig
const std = @import("std");

fn max(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}

test "comptime parameter" {
    const x = max(u32, 3, 5);
    try std.testing.expect(x == 5);
}
```

## Comptime for performance
Patterns:
- Specialize on lane count and unroll factors (`comptime lanes`, `comptime unroll`).
- Generate lookup tables at comptime (e.g., classification maps, shuffle indices).
- Prefer `comptime if` for CPU/arch dispatch (`builtin.cpu.arch`) when you need different kernels.

### Comptime specialization example (SIMD dot product)
```zig
const std = @import("std");

fn Dot(comptime lanes: usize) type {
    return struct {
        pub fn dot(a: []const f32, b: []const f32) f32 {
            const V = @Vector(lanes, f32);

            var i: usize = 0;
            var acc: V = @splat(0);

            while (i + lanes <= a.len) : (i += lanes) {
                const av: V = a[i..][0..lanes].*;
                const bv: V = b[i..][0..lanes].*;
                acc += av * bv;
            }

            var total: f32 = @reduce(.Add, acc);
            while (i < a.len) : (i += 1) total += a[i] * b[i];
            return total;
        }
    };
}

pub fn dotAuto(a: []const f32, b: []const f32) f32 {
    const lanes = std.simd.suggestVectorLength(f32) orelse 1;
    return Dot(lanes).dot(a, b);
}
```

## Build essentials (`build.zig`)
```zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "my-app",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    b.installArtifact(exe);

    const run_cmd = b.addRunArtifact(exe);
    run_cmd.step.dependOn(b.getInstallStep());

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);

    if (b.args) |args| run_cmd.addArgs(args);
}
```

## Package management (`build.zig.zon`)
```zig
.{
    .name = "my-project",
    .version = "0.1.0",
    .dependencies = .{
        .@"some-package" = .{
            .url = "https://github.com/user/package/archive/main.tar.gz",
            .hash = "1220abcdef...",
        },
    },
    .paths = .{ "build.zig", "build.zig.zon", "src" },
}
```

## Memory / allocators (performance-first)
Rules of thumb:
- Debugging correctness/leaks: `std.testing.allocator` in tests, or `std.heap.DebugAllocator` in apps.
- Throughput + multithreading (ReleaseFast): `std.heap.smp_allocator` (singleton, designed for MT + ReleaseFast).
- Short-lived "build a result then throw away": `std.heap.ArenaAllocator` on top of a fast backing allocator.
- Scratch buffers: `std.heap.FixedBufferAllocator` or `std.heap.stackFallback(N, fallback)`.
- Fixed-size objects: `std.heap.MemoryPool` / `std.heap.MemoryPoolAligned`.

### Debug allocator (leak checking)
```zig
const std = @import("std");

pub fn main() !void {
    var dbg = std.heap.DebugAllocator(.{}){};
    defer _ = dbg.deinit();
    const allocator = dbg.allocator();

    const bytes = try allocator.alloc(u8, 100);
    defer allocator.free(bytes);
}
```

### Smp allocator + arena reset (hot loop friendly)
```zig
const std = @import("std");
const builtin = @import("builtin");

pub fn buildManyThings() !void {
    const backing = if (builtin.single_threaded) std.heap.page_allocator else std.heap.smp_allocator;

    var arena = std.heap.ArenaAllocator.init(backing);
    defer arena.deinit();
    const a = arena.allocator();

    var i: usize = 0;
    while (i < 1000) : (i += 1) {
        _ = arena.reset(.retain_capacity);
        _ = try a.alloc(u8, 4096);
    }
}
```

## Inspecting codegen / benchmarking
- Emit assembly: `zig build-exe -O ReleaseFast -mcpu=native -femit-asm src/main.zig`
- Emit optimized LLVM IR: `zig build-exe -O ReleaseFast -mcpu=native -femit-llvm-ir src/main.zig` (LLVM extensions)
- Track compile time: `--time-report`
- Prevent DCE in benches: `std.mem.doNotOptimizeAway(x)`
- Time loops: `std.time.Timer`, `std.time.nanoTimestamp`

### Benchmark contract for parser work
- Benchmark only the parse/iterate loop; exclude file download/decompression and setup noise.
- Keep buffer size fixed in reports (for example 64 KiB) so runs are comparable.
- Report wall time and at least one hardware counter set (branch misses, cache misses) plus RSS.
- Capture machine + OS + compiler version + dataset mix in the benchmark notes.

## Zero-copy parsing playbook
Principles:
- Treat input as immutable bytes; parse into views, not copies.
- Make ownership explicit (borrowed vs owned).
- Store spans/offsets into a stable base buffer.
- Never return slices into temporary buffers.

### Iterator lifetime + streaming buffer contract (CSV-class)
- Prefer field iterators for hot paths (one field per `next()`), then build record views on top.
- Document lifetime explicitly: returned field slices are valid until the next `next()` call unless
  they reference caller-owned stable backing storage.
- For streaming input, slide partial tokens to the front before refill (`@memmove`), then continue.
- If a token exceeds buffer capacity, fail fast with an explicit error (`error.FieldTooLong`) instead
  of truncating or reallocating in the hot loop.

```zig
fn refillKeepingTail(buf: []u8, head: *usize, tail: *usize, reader: anytype) !usize {
    if (head.* > 0 and head.* < tail.*) {
        const rem = tail.* - head.*;
        @memmove(buf[0..rem], buf[head.*..tail.*]);
        head.* = 0;
        tail.* = rem;
    } else if (head.* == tail.*) {
        head.* = 0;
        tail.* = 0;
    }

    if (tail.* == buf.len) return error.FieldTooLong;
    const n = try reader.read(buf[tail.*..]);
    tail.* += n;
    return n;
}
```

### Quoted-field finite-state path (robust CSV)
- Split early into unquoted and quoted paths; keep unquoted path branch-light.
- In quoted mode, treat `""` as escaped quote, then require delimiter/newline/end-of-input after
  the closing quote.
- Track `needs_unescape` during scan and only unescape when necessary.
- Normalize CRLF with integer booleans in hot loops to avoid extra branches.

```zig
const has_lf: usize = @intFromBool(end > start and buf[end - 1] == '\n');
const has_cr: usize = @intFromBool(end > start + has_lf and buf[end - 1 - has_lf] == '\r');
const trimmed_end = end - has_lf - has_cr;
```

### Borrowed/owned token (copy-on-write escape hatch)
```zig
const std = @import("std");

pub const ByteView = union(enum) {
    borrowed: []const u8,
    owned: []u8,

    pub fn slice(self: ByteView) []const u8 {
        return switch (self) {
            .borrowed => |s| s,
            .owned => |s| s,
        };
    }

    pub fn toOwned(self: ByteView, allocator: std.mem.Allocator) ![]u8 {
        return switch (self) {
            .owned => |s| s,
            .borrowed => |s| try allocator.dupe(u8, s),
        };
    }

    pub fn deinit(self: *ByteView, allocator: std.mem.Allocator) void {
        if (self.* == .owned) allocator.free(self.owned);
        self.* = .{ .borrowed = &.{} };
    }
};
```

### POSIX mmap (stable base buffer)
```zig
const std = @import("std");

pub const MappedFile = struct {
    data: []const u8,
    owns: bool,

    pub fn open(path: []const u8) !MappedFile {
        const file = try std.fs.cwd().openFile(path, .{});
        defer file.close();
        const size = (try file.stat()).size;
        const map = try std.posix.mmap(
            null,
            size,
            std.posix.PROT.READ,
            .{ .TYPE = .PRIVATE },
            file.handle,
            0,
        );
        return .{ .data = map, .owns = true };
    }

    pub fn close(self: *MappedFile) void {
        if (self.owns) std.posix.munmap(self.data);
        self.* = .{ .data = &.{}, .owns = false };
    }
};
```

### Span-based parsing (offsets, not copies)
```zig
const Span = struct {
    base: []const u8,
    start: usize,
    len: usize,

    pub fn slice(self: Span) []const u8 {
        return self.base[self.start..][0..self.len];
    }
};
```

## Testing
- Run correctness tests in Debug or ReleaseSafe; run perf checks in ReleaseFast.
- Leak detection: use `std.testing.allocator` and `defer` frees.
- Prefer differential tests (reference vs optimized) and metamorphic invariants (roundtrip, monotonicity).
- Allocation counting: wrap an allocator and assert zero allocations for a "zero-copy" path.
- OOM injection: run under `std.testing.FailingAllocator`.
- Exhaustive OOM: `std.testing.checkAllAllocationFailures`.

## Fuzz testing (required)
### Built-in fuzzer (default, Zig 0.15.2)
Use `std.testing.fuzz` in a `test` block and run:
`zig build test --fuzz` (optionally `-Doptimize=ReleaseSafe`).

Consult `zig build test --help` for version-specific `--fuzz` flags.

### macOS caveat (Zig 0.15.2)
Try `zig build test --fuzz` on macOS first. If it crashes during startup
(`InvalidElfMagic` observed), skip local fuzzing for that run, keep
`std.testing.fuzz` tests in-tree, and run the fuzz step on Linux/CI or via an
external harness; use `zig test` locally for smoke coverage.

### Fuzz target rules (make it fuzzer-friendly)
- Deterministic: no timers, threads, or internal RNG (the fuzzer is the RNG).
- Total: accept any input bytes; never read out of bounds; no UB-by-assumption.
- Bounded: cap pathological work (length limits, recursion depth, max allocations).
- Isolated: no global mutable state (or fully reset per call).
- Assert properties, not vibes: reference equivalence, roundtrips, monotonicity, invariants.

### Differential fuzzing (recommended)
Make your fuzz target assert equivalence between a small reference implementation and the
optimized kernel. This is the fastest route to algorithmic correctness.

Compile-checked template: `codex/skills/zig/references/fuzz_differential.zig`.

Template:
```zig
const std = @import("std");

fn refCountOnes(bytes: []const u8) u64 {
    var n: u64 = 0;
    for (bytes) |b| n += @popCount(b);
    return n;
}

fn fastCountOnes(bytes: []const u8) u64 {
    // Replace with the optimized version (SIMD/threads/etc).
    return refCountOnes(bytes);
}

fn fuzzTarget(_: void, input: []const u8) !void {
    const ref = refCountOnes(input);
    const got = fastCountOnes(input);
    try std.testing.expectEqual(ref, got);
}

test "fuzz target" {
    try std.testing.fuzz({}, fuzzTarget, .{});
}
```

### Allocation-failure fuzzing (mandatory for allocators)
`std.testing.checkAllAllocationFailures` exhaustively injects `error.OutOfMemory` across
all allocations in a test function. The test function must take an allocator as its first
argument, return `!void`, and reset shared state each run.

```zig
const std = @import("std");

fn parseWithAlloc(alloc: std.mem.Allocator, bytes: []const u8) !void {
    _ = alloc;
    _ = bytes;
}

test "allocation failure fuzz" {
    const input = "seed";
    try std.testing.checkAllAllocationFailures(
        std.testing.allocator,
        parseWithAlloc,
        .{input},
    );
}
```

### Allocator pressure tricks (recommended)
- Cap per-allocation size relative to input length to surface pathological allocations.
- Wrap with `std.testing.FailingAllocator` to validate `errdefer` and cleanup paths.
- Persist interesting inputs under `testdata/fuzz/` and promote crashes to regression tests.

### Corpus + regression workflow (required)
- When fuzz finds a crash/mismatch, save the input under `testdata/fuzz/<target>/`.
- Add a deterministic regression test using `@embedFile`.

```zig
test "regression: fuzz crash" {
    const input = @embedFile("testdata/fuzz/parser/crash-<id>.bin");
    try std.testing.expectEqual(refCountOnes(input), fastCountOnes(input));
}
```

### Fast in-tree randomized fuzz (smoke; complements `--fuzz`)
Use randomized inputs inside `test` blocks for cheap, always-on coverage.

```zig
const std = @import("std");

fn parse(bytes: []const u8) !void {
    _ = bytes;
}

test "fuzz parse" {
    var prng = std.rand.DefaultPrng.init(0x9e3779b97f4a7c15);
    const rng = prng.random();

    var buf: [4096]u8 = undefined;
    var i: usize = 0;
    while (i < 10_000) : (i += 1) {
        const len = rng.intRangeAtMost(usize, 0, buf.len);
        const input = buf[0..len];
        rng.bytes(input);
        _ = parse(input) catch {};
    }
}
```

### External harnesses (optional)
If you need AFL++/libFuzzer infrastructure (shared corpora, distributed fuzzing, custom
instrumentation), export a C ABI entrypoint and drive it from an external harness.
Example outline:
- Export a stable entrypoint: `export fn fuzz_target(ptr: [*]const u8, len: usize) void`.
- Build a static library with `zig build-lib`.
- Link it from an external harness (AFL++ via `cargo-afl`) and run with a seed corpus.

## C interop
```zig
const c = @cImport({
    @cInclude("stdio.h");
});

pub fn main() void {
    _ = c.printf("Hello from C!\n");
}
```

## Pitfalls
- Multithreading: false sharing, oversubscription, shared allocator contention.
- SIMD: misaligned loads on some targets, reading past the end, non-associative FP reductions.
- SIMD bitmasks: when you `@bitCast` `@Vector(N, bool)` to an int mask, bit 0 is lane/index 0.
- `std.heap.GeneralPurposeAllocator` is deprecated (alias of `DebugAllocator`); keep for existing code, prefer explicit allocator choices for new code.
- Make ownership explicit; always free heap allocations.
- Avoid returning slices backed by stack memory.
- `[*c]T` is nullable; `[*]T` is non-null.
- Use `zig fetch --save` to populate `build.zig.zon` hashes.

## Activation cues
- "zig" / "ziglang" / ".zig"
- "build.zig" / "build.zig.zon"
- "zig build" / "zig test"
- "comptime" / "allocator" / "@typeInfo" / "@compileError"
- "SIMD" / "@Vector" / "std.simd"
- "thread" / "std.Thread" / "Thread.Pool" / "WaitGroup"

Overview

This skill helps with Zig code and toolchain work focused on correctness-first development, fuzzing, performance, and cross-platform builds. It targets Zig 0.15.2 and guides edits to .zig files, build.zig, SIMD/threading kernels, comptime metaprogramming, allocator ownership, and C interop. The workflow enforces automated tests and fuzzing as the baseline signal for changes.

How this skill works

I inspect Zig sources, build scripts, tests, and fuzz harnesses to ensure correctness and reproducible behavior. I recommend and apply patterns for unit tests, std.testing.fuzz integration, allocation-failure checks, and differential fuzzing between reference and optimized implementations. For performance work I follow a staged approach: algorithm → layout → SIMD → threads → micro‑tuning, and provide templates and command snippets for building, running, and benchmarking.

When to use it

  • Editing or reviewing .zig files or build.zig changes
  • Diagnosing compile, test, runtime, or fuzz failures
  • Implementing or validating comptime, reflection, or codegen
  • Writing or hardening parsers, allocator-using code, or C interop via @cImport
  • Optimizing hot paths with SIMD (std.simd/@Vector) or threading (std.Thread.Pool)
  • Cross-compilation, formatting, or build/test automation for Zig 0.15.2

Best practices

  • Treat correctness as non-negotiable: every change must have at least one correctness signal (tests or fuzzing).
  • Use std.testing.fuzz and zig build test --fuzz as the default harness; prefer differential fuzzing against a simple reference implementation.
  • Run fuzzing in Debug/ReleaseSafe and benchmark separately in ReleaseFast; persist seed corpus and repro cases.
  • Optimize in stages: fix algorithmic issues first, then tune data layout, then add explicit SIMD, then parallelize with a thread pool.
  • Make hot loops allocation-free and prefer contiguous slices and SoA layouts; guard vector paths and handle tails with scalar fallbacks.

Example use cases

  • Add or fix a failing unit test and expand a fuzz harness to reproduce the bug across builds.
  • Optimize a numeric kernel: add a reference implementation, differential-fuzz, then implement an @Vector SIMD path with scalar tail-handling.
  • Implement cross-compilation targets and CI steps: zig build -Dtarget=... and ensure fuzz runs on Linux CI when local fuzzing is impossible.
  • Harden parser/allocator code: add std.testing.checkAllAllocationFailures and integrated fuzzing to assert safety under OOM.
  • Write a comptime-based derive pipeline to generate equality/hash/format implementations and validate with compile-time checks.
  • Resolve C interop issues by auditing @cImport usage, checking ABI layout with @sizeOf/@alignOf/@offsetOf, and adding tests.

FAQ

Which Zig version do you target by default?

Default baseline is Zig 0.15.2; no compatibility work for older versions unless explicitly requested.

What is the required minimum correctness signal for a change?

Every change must include at least one correctness signal: unit test, fuzz harness, or allocation-failure checks. For safety-sensitive code, fuzzing is preferred.