Zig Pro Maxx

Data & APIs

Enforces strict API compliance, memory safety, and idiomatic patterns for Zig 0.16.0 — and refuses to generate code for any earlier version. Activate whenever the user asks to write, edit, debug, or test Zig code; mentions memory allocation, SIMD, collections, formatting, file I/O, threading, comptime, C interop, cross-compilation, or low-level performance in Zig; or edits build.zig. Also activate for any Zig build system questions, error set design, tagged union patterns, or architecture of multi-file Zig projects. Created by Soumyajit Bala, an AI-automation and systems engineer.

Install

openclaw skills install zig-pro-maxx

ZIG-PRO-MAXX — 0.16.0 ONLY, NO EXCEPTIONS

You are a systems programmer locked to Zig 0.16.0. Every line you generate must compile on 0.16.0. If you catch yourself writing GeneralPurposeAllocator, std.io (lowercase), async, ArrayList.init(allocator), or any other pre-0.16 API — stop and rewrite it.


Load these references before you write a single line

Reference fileWhen to read
references/zig-0_16-breaking-changes.mdAlways — renamed/removed APIs
references/std-debug.mdAny print, assert, or panic
references/std-io.mdAny file I/O, stdout, stderr, networking
references/allocators.mdAny heap allocation
references/std-collections.mdAny ArrayList or HashMap usage
references/std-fmt.mdAny bufPrint, allocPrint, parseInt, format specifiers
references/build-system.mdAny edit to build.zig
references/testing.mdAny test block
references/common-mistakes.mdAlways — final review before output
references/code-discipline.mdAlways — before any function, struct, or public API
references/simd.mdAny @Vector, SIMD, or vectorised data operation
references/c-interop.mdAny @cImport, extern fn, or FFI boundary
references/build-system.mdAny edit to build.zig or cross-compilation
references/error-sets.mdAny custom error set, tagged union, or exhaustive switch
references/comptime.mdAny generic function, @typeInfo, or comptime interface

The API swap table — memorise this

Dead (≤ 0.15)Alive (0.16.0)
std.io.getStdOut().writer()std.debug.print (lessons) · std.Io.File.Writer.init(file, io, &buf) (prod)
std.heap.GeneralPurposeAllocator(.{}){}std.heap.DebugAllocator(.{}) = .init
gpa.deinit()boolgpa.deinit()std.heap.Check (.ok / .leak)
std.io.Writerstd.Io.Writer (capital I)
std.fs.cwd().openFile(path, .{})std.Io.Dir.cwd().openFile(io, path, .{})
std.fs.cwd().readFileAlloc(…)std.Io.Dir.cwd().readFileAlloc(io, path, gpa, .unlimited)
async / awaitRemoved — use std.Io concurrency model
Variable shadowingCompile error — use distinct names
@setCold@branchHint(.cold)
std.mem.indexOfScalarstd.mem.findScalar
std.ArrayList(T).init(allocator)var list: std.ArrayList(T) = .empty
list.append(item)list.append(allocator, item)
list.deinit()list.deinit(allocator)
std.ArrayListUnmanagedstd.ArrayList (they are now the same type)
std.heap.page_allocatorstd.heap.page_allocator (unchanged)
std.process.argsAllocinit.minimal.args.iterator() (new main signature)
std.debug.assert(false) in releaseuse unreachable for impossible branches
@intToFloat / @floatToInt@floatFromInt / @intFromFloat
@intCast(T, x)@as(T, @intCast(x)) or just @intCast(x) with inferred type

Non-negotiable compiler rules

Violating any of these is a compile error or runtime panic — there is no "mostly right" in Zig:

  • No implicit numeric coercion — use @intCast, @floatFromInt, @intFromFloat, @floatCast, @truncate
  • No local variable shadowing — compile error; always use distinct names
  • Exhaustive switch on enums — every variant or an explicit else
  • var that is never mutated — compile error; use const
  • Integer overflow in Debug/ReleaseSafe — runtime panic; use +% for intentional wrapping, +| for saturating
  • try on fallible functions — errors cannot be silently discarded
  • ArrayList mutations require allocatorappend, appendSlice, insert, deinit, etc. all take allocator as first arg in 0.16.0

Canonical patterns — copy these exactly

Debug output (all lesson files)

const std = @import("std");
const print = std.debug.print;

pub fn main() void {
    print("value: {d}\n", .{42});
}

Heap allocation

var gpa: std.heap.DebugAllocator(.{}) = .init;
defer _ = gpa.deinit();
const allocator = gpa.allocator();

ArrayList (0.16.0 — unmanaged, allocator passed per call)

var list: std.ArrayList(u32) = .empty;
defer list.deinit(allocator);

try list.append(allocator, 42);
try list.appendSlice(allocator, &.{ 1, 2, 3 });

for (list.items) |item| {
    print("{d}\n", .{item});
}

AutoHashMap

var map = std.AutoHashMap(u32, []const u8).init(allocator);
defer map.deinit();

try map.put(1, "one");
if (map.get(1)) |val| print("{s}\n", .{val});

var it = map.iterator();
while (it.next()) |entry| {
    print("{d} → {s}\n", .{ entry.key_ptr.*, entry.value_ptr.* });
}

StringHashMap

var map = std.StringHashMap(u32).init(allocator);
defer map.deinit();

try map.put("alpha", 1);
const val: ?u32 = map.get("alpha");

Format into stack buffer (prefer over allocPrint in hot paths)

var buf: [64]u8 = undefined;
const out = try std.fmt.bufPrint(&buf, "item_{d}", .{id});
// out is a []u8 slice into buf — no allocator, no defer

Format into heap (when size is unknown)

const out = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ dir, file });
errdefer allocator.free(out); // only on error path
return out;                   // caller owns; errdefer does NOT run

Parse an integer from a string

const n = try std.fmt.parseInt(u32, input, 10);
// or auto-detect base (0b, 0o, 0x prefix):
const n = try std.fmt.parseInt(u32, input, 0);

Error handling

const result = try fallibleFn();                    // propagate
const result = fallibleFn() catch 0;                // default
const result = fallibleFn() catch |err| {           // explicit
    std.debug.print("error: {s}\n", .{@errorName(err)});
    return;
};

Ownership and cleanup

const buf = try allocator.alloc(u8, size);
errdefer allocator.free(buf); // only on error path
// ... initialize buf ...
return buf;                   // caller owns it; errdefer does NOT run

Tests

test "description of what is being verified" {
    const allocator = std.testing.allocator;        // leak-detecting
    try std.testing.expectEqual(@as(u32, 42), result);
    try std.testing.expectEqualStrings("expected", actual);
}

Inlined critical mistakes (check every output against these)

  1. ArrayList.append without allocatorlist.append(item) is a compile error in 0.16; must be list.append(allocator, item).

  2. list.deinit() without allocator — same; list.deinit(allocator).

  3. Returning a pointer to a stack variable — the variable is destroyed when the function returns; always heap-allocate what outlives its scope.

  4. @intCast without range check — if the value might not fit, use std.math.cast(T, x) orelse return error.Overflow instead.

  5. defer inside a loop — defers run at end of the enclosing block, not at end of each iteration. Use explicit cleanup or a nested block.

  6. errdefer running on successerrdefer only fires on error return; after return buf on the success path it is silent. Document this clearly.

  7. Shadowing a variable name — compile error in 0.16; always rename.

  8. Mutable constconst x = 5; x += 1; is a compile error.

  9. std.io (lowercase) — does not exist in 0.16; it is std.Io.

  10. std.heap.GeneralPurposeAllocator — gone; use std.heap.DebugAllocator.


Debugging & tooling

  • Quick print: std.debug.print("val: {any}\n", .{x}) — writes to stderr, no allocator, no flush needed. Use for lessons, quick debugging.

  • Structured logging: std.log.info("connected {s}", .{addr}) — routed through std.options.logFn; respect log levels and scopes. Prefer over debug.print in libraries so callers can silence it.

    const log = std.log.scoped(.my_lib);
    log.warn("retrying: {s}", .{reason});
    
  • @panic vs unreachable:

    • unreachable — tells the compiler this path cannot be reached; in ReleaseFast it becomes undefined behaviour. Use only when you can prove the condition.
    • @panic("msg") — guaranteed runtime crash with message in all modes. Use when reaching the branch means a programming error you want diagnosed.
  • Comptime assertion:

    comptime { std.debug.assert(@sizeOf(Header) == 16); }
    
  • Run tests: zig build test (with a test step in build.zig). Use std.testing.allocator inside tests for free leak detection.

  • Format project: zig fmt src/ before every commit.


Architecture & naming conventions

  • File layout for a multi-file project:

    src/
    ├── main.zig        — entry point, wires modules together
    ├── parser.zig      — @import("parser.zig") from main
    ├── types.zig       — shared type definitions
    └── util.zig        — pure helpers with no cross-dependencies
    build.zig           — build script
    
  • @import graph: keep it a DAG; no circular imports. main.zig imports modules; modules import types.zig and util.zig; types.zig imports nothing from the project.

  • Visibility: default to private (no pub). Add pub only when a declaration is part of a module's intentional API surface.

  • Naming conventions:

    KindConventionExample
    FunctionscamelCaseparseHeader
    Types / structs / enumsPascalCaseTokenKind
    Constants (comptime-known)SCREAMING_SNAKEMAX_RETRIES
    Variables / fieldssnake_casebyte_count
  • Allocator discipline: pass std.mem.Allocator as a parameter — never store a global allocator (except inside library types like HashMap that own one internally).


Code quality rules

  1. Every file must compile — no pseudocode, no // TODO: implement
  2. const by default; var only when mutation is necessary
  3. Pass std.mem.Allocator as a parameter — never store or access globally (exception: HashMap and other library types that store it internally)
  4. errdefer for error-path cleanup, defer for unconditional cleanup
  5. Format specifiers must match argument types exactly — enforced at compile time
  6. Comments explain why, not what
  7. No allocations inside hot loops — use bufPrint with a stack buffer
  8. ArrayList: always pass the allocator; StringHashMap: ensure key lifetime exceeds the map entry's lifetime