AgentStack
SKILL verified MIT Self-run

Zig

skill-nzrsky-zig-skills-zig · by nzrsky

Up-to-date Zig programming language patterns for version 0.16.0. Use when writing, reviewing, or debugging Zig code, working with build.zig and build.zig.zon files, or using comptime metaprogramming. Critical for avoiding outdated patterns from training data - especially std.net→std.Io.net (requires Io instance), std.time timestamps removed (use clock_gettime), std.Thread.Mutex/Condition/sleep re…

No reviews yet
0 installs
12 views
0.0% view→install

Install

$ agentstack add skill-nzrsky-zig-skills-zig

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Zig? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Zig Language Reference (v0.16.0)

Zig evolves rapidly. Training data contains outdated patterns that cause compilation errors. This skill documents breaking changes and correct modern patterns.

Version coverage: 0.16.0 (current) with migration notes from 0.15.x and 0.14.x.

Design Principles

Type-First Development

Define types and function signatures before implementation. Let the compiler guide completeness:

  1. Define data structures (structs, unions, error sets)
  2. Define function signatures (parameters, return types, error unions)
  3. Implement to satisfy types
  4. Validate at compile-time

Make Illegal States Unrepresentable

Use Zig's type system to prevent invalid states at compile time:

  • Tagged unions over structs with optional fields — prevent impossible state combinations
  • Explicit error sets over anyerror — document exactly which failures can occur
  • Distinct types via enum(u64) { _ } — prevent mixing up IDs (userid vs orderid)
  • Comptime validation with @compileError() — catch invalid configurations at build time

Module Structure

Larger cohesive files are idiomatic in Zig. Keep related code together — tests alongside implementation, comptime generics at file scope, visibility controlled by pub. Split files only for genuinely separate concerns. The std library demonstrates this with files like std/mem.zig containing thousands of cohesive lines.

Memory Ownership

  • Pass allocators explicitly — never use global state for allocation
  • Use defer immediately after acquiring a resource — cleanup next to acquisition
  • Name allocators by contract: gpa (caller must free), arena (bulk-free at boundary), scratch (never escapes)
  • Prefer const over var — immutability signals intent and enables optimizations
  • Prefer slices over raw pointers — bounds safety

Critical: Removed Features (0.15.x)

usingnamespace - REMOVED

// WRONG - compile error
pub usingnamespace @import("other.zig");

// CORRECT - explicit re-export
const other = @import("other.zig");
pub const foo = other.foo;

async/await - REMOVED

Keywords removed from language. Async I/O support is planned for future releases.

std.BoundedArray - REMOVED

Use std.ArrayList with initBuffer:

var buffer: [8]i32 = undefined;
var stack = std.ArrayList(i32).initBuffer(&buffer);

std.RingBuffer, std.fifo.LinearFifo - REMOVED

Use std.Io.Reader/std.Io.Writer ring buffers instead.

std.io.SeekableStream, std.io.BitReader, std.io.BitWriter - REMOVED

std.fmt.Formatter - REMOVED

Replaced by std.fmt.Alt.

Undefined Behavior Restrictions (0.15.x)

Arithmetic on undefined is now illegal. Only operators that can never trigger Illegal Behavior permit undefined as operand.

// WRONG - compile error in 0.15.x
var n: usize = undefined;
while (condition) : (n += 1) {}  // ERROR: use of undefined value

// CORRECT - explicit initialization required
var n: usize = 0;
while (condition) : (n += 1) {}

// OK - space reservation (no arithmetic)
var buffer: [256]u8 = undefined;

Critical: Networking Removed — std.netstd.Io.net (0.16)

std.net is completely removed in 0.16. Replaced by std.Io.net, which requires an Io instance.

Accept Loop

// WRONG (0.15) — std.net removed
const addr = std.net.Address.parseIp4(host, port) catch unreachable;
var server = addr.listen(.{ .reuse_address = true }) catch unreachable;
const conn = server.accept() catch continue;
defer conn.stream.close();

// CORRECT (0.16) — std.Io.net with Io instance
const addr = try std.Io.net.IpAddress.parse(host, port);
var server = try addr.listen(io, .{ .reuse_address = true });
const stream = try server.accept();  // returns Stream directly, no .stream wrapper
defer stream.close(io);              // close() now takes io

Io Runtime Setup

// Create Io instance at startup, thread it through your program
var threaded = std.Io.Threaded.init(std.heap.c_allocator);
var io: std.Io = threaded.io();

Stream Changes

// stream.handle → stream.socket.handle
std.posix.setsockopt(stream.socket.handle, ...);

// Io.net.Stream has NO .read() or .writeAll() — use raw C calls for blocking I/O:
extern "c" fn write(fd: c_int, buf: [*]const u8, n: usize) isize;

fn writeAll(stream: std.Io.net.Stream, data: []const u8) !void {
    var rem = data;
    while (rem.len > 0) {
        const n = write(stream.socket.handle, rem.ptr, rem.len);
        if (n  `std.Io.Writer.Discarding` (has `.fullCount()`)
- `BufferedWriter` -> buffer provided to `.writer(&buf)` call
- Allocating output -> `std.Io.Writer.Allocating`

### `std.io.fixedBufferStream` Removed (0.16)
```zig
// WRONG (0.16) — std.io (lowercase) removed entirely
var buf: [512]u8 = undefined;
var stream = std.io.fixedBufferStream(&buf);
try std.fmt.format(stream.writer(), "{d}", .{value});
const result = stream.getWritten();

// CORRECT — use std.fmt.bufPrint directly
var buf: [512]u8 = undefined;
const result = try std.fmt.bufPrint(&buf, "{d}", .{value});

Vtable Writer Type Changed (0.16)

// WRONG — *std.io.Writer (lowercase)
fn drain(w: *std.io.Writer) error{WriteFailed}!usize { ... }

// CORRECT — *std.Io.Writer (capital)
fn drain(w: *std.Io.Writer) error{WriteFailed}!usize { ... }

Critical: Build System (0.15.x)

root_source_file is REMOVED from addExecutable/addLibrary/addTest. Use root_module:

// WRONG - removed field
b.addExecutable(.{
    .name = "app",
    .root_source_file = b.path("src/main.zig"),  // ERROR
    .target = target,
});

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

Module imports changed:

// WRONG (old API)
exe.addModule("helper", helper_mod);

// CORRECT
exe.root_module.addImport("helper", helper_mod);

Libraries: addSharedLibraryaddLibrary with .linkage:

// WRONG - removed function
const lib = b.addSharedLibrary(.{ .name = "mylib", ... });

// CORRECT - unified addLibrary with linkage field
const lib = b.addLibrary(.{
    .name = "mylib",
    .linkage = .dynamic,  // or .static (default)
    .root_module = b.createModule(.{
        .root_source_file = b.path("src/lib.zig"),
        .target = target,
        .optimize = optimize,
    }),
});

Adding dependency modules:

const dep = b.dependency("lib", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("lib", dep.module("lib"));

Compile.* Methods Moved to Module.* (0.16)

In 0.16, methods like addIncludePath, addLibraryPath, linkSystemLibrary, addCSourceFile moved from the Compile step to the module:

// WRONG (0.16) — methods no longer on Compile
lib.addIncludePath(.{ .cwd_relative = path });
lib.linkSystemLibrary("foo");
lib.addCSourceFile(.{ .file = b.path("shim.c"), .flags = &.{} });

// CORRECT — use root_module
lib.root_module.addIncludePath(.{ .cwd_relative = path });
lib.root_module.linkSystemLibrary("foo", .{});  // note: now takes options struct
lib.root_module.addCSourceFile(.{ .file = b.path("shim.c"), .flags = &.{} });

Common misleading error: no field or member function named 'addIncludePath' in 'Build.Step.Compile'. The note about .* is wrong — the fix is lib.root_module.addIncludePath(...).

See [std.Build reference](references/std-build.md) for complete build system documentation.

Critical: Container Initialization

Never use .{} for containers. Use .empty or .init:

// WRONG - deprecated
var list: std.ArrayList(u32) = .{};
var gpa: std.heap.DebugAllocator(.{}) = .{};

// CORRECT - use .empty for empty collections
var list: std.ArrayList(u32) = .empty;
var map: std.AutoHashMapUnmanaged(u32, u32) = .empty;

// CORRECT - use .init for stateful types with internal config
var gpa: std.heap.DebugAllocator(.{}) = .init;
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);

ArrayList: Unmanaged by Default

The old std.ArrayList (with allocator stored in struct) is now std.array_list.Managed. The new std.ArrayList is unmanaged — no allocator field, pass allocator to every method:

// NEW default: unmanaged (no allocator field)
var list: std.ArrayList(u32) = .empty;
try list.append(allocator, 42);
list.deinit(allocator);

// If you want old behavior (allocator in struct), use Managed:
var list = std.array_list.Managed(u32).init(allocator);
try list.append(42);  // no allocator arg needed
list.deinit();

HashMap: Managed vs Unmanaged

Same pattern applies — unmanaged variants require allocator per operation:

// Unmanaged (no internal allocator)
var map: std.StringHashMapUnmanaged(u32) = .empty;
try map.put(allocator, "key", 42);
map.deinit(allocator);

// Managed (stores allocator internally)
var map = std.StringHashMap(u32).init(allocator);
try map.put("key", 42);
map.deinit();

ArrayListUnmanaged Empty Init (0.16)

In 0.16, .{} zero-init no longer works for ArrayListUnmanaged — explicit fields required:

// WRONG (0.16) — .{} no longer zero-inits correctly
._list = .{},

// CORRECT — explicit fields
._list = .{ .items = &.{}, .capacity = 0 },

Naming Changes

  • std.ArrayListUnmanaged -> std.ArrayList (Unmanaged is now default, old name deprecated)
  • std.heap.GeneralPurposeAllocator -> std.heap.DebugAllocator (GPA alias still works)

Linked Lists: Generic Parameter Removed

// WRONG - old API
const Node = std.DoublyLinkedList(MyData).Node;

// CORRECT - non-generic, use @fieldParentPtr
const MyNode = struct {
    node: std.DoublyLinkedList.Node,
    data: MyData,
};
// Access: const my_node = @fieldParentPtr("node", node_ptr);

Process API: Term is Tagged Union

// WRONG - direct field access removed
if (result.term.Exited != 0) {}

// CORRECT - pattern match
switch (result.term) {
    .exited => |code| if (code != 0) { /* handle */ },
    else => {},
}

Critical: Format Strings (0.15.x)

{f} required to call format methods:

// WRONG - ambiguous error
std.debug.print("{}", .{std.zig.fmtId("x")});

// CORRECT
std.debug.print("{f}", .{std.zig.fmtId("x")});

Format method signature changed:

// OLD - wrong
pub fn format(self: @This(), comptime fmt: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void

// NEW - correct
pub fn format(self: @This(), writer: *std.Io.Writer) std.Io.Writer.Error!void

Breaking Changes (0.14.0+)

@branchHint replaces @setCold

// WRONG
@setCold(true);

// CORRECT
@branchHint(.cold);  // Must be first statement in block

@export takes pointer

// WRONG
@export(foo, .{ .name = "bar" });

// CORRECT
@export(&foo, .{ .name = "bar" });

Inline asm clobbers are typed

// WRONG
: "rcx", "r11"

// CORRECT
: .{ .rcx = true, .r11 = true }

@fence - REMOVED

Use stronger atomic orderings or RMW operations instead.

@typeInfo fields now lowercase

// WRONG - old PascalCase (compile error)
if (@typeInfo(T) == .Struct) { ... }
if (@typeInfo(T) == .Slice) { ... }
if (@typeInfo(T) == .Int) { ... }

// CORRECT - lowercase, keywords escaped with @""
if (@typeInfo(T) == .@"struct") { ... }
if (@typeInfo(T) == .@"enum") { ... }
if (@typeInfo(T) == .@"union") { ... }
if (@typeInfo(T) == .@"opaque") { ... }
if (@typeInfo(T) == .slice) { ... }
if (@typeInfo(T) == .int) { ... }
if (@typeInfo(T) == .bool) { ... }
if (@typeInfo(T) == .pointer) { ... }

// Accessing fields:
const fields = @typeInfo(T).@"struct".fields;
const tag_type = @typeInfo(T).@"enum".tag_type;

Decl Literals (0.14.0+)

.identifier syntax works for declarations:

const S = struct {
    x: u32,
    const default: S = .{ .x = 0 };
    fn init(v: u32) S { return .{ .x = v }; }
};

const a: S = .default;      // S.default
const b: S = .init(42);     // S.init(42)
const c: S = try .init(1);  // works with try

Labeled Switch (0.14.0+)

State machines use continue :label:

state: switch (initial) {
    .idle => continue :state .running,
    .running => if (done) break :state result else continue :state .running,
    .error => return error.Failed,
}

Non-exhaustive Enum Switch (0.15.x)

Can mix explicit tags with _ and else:

switch (value) {
    .a, .b => {},
    else => {},  // other named tags
    _ => {},     // unnamed integer values
}

Critical: HTTP API Reworked (0.15.x)

HTTP client/server completely restructured — depends only on I/O streams, not networking:

// Server now takes Reader/Writer interfaces, not connection directly
var recv_buffer: [4000]u8 = undefined;
var send_buffer: [4000]u8 = undefined;
var conn_reader = connection.stream.reader(&recv_buffer);
var conn_writer = connection.stream.writer(&send_buffer);
var server = std.http.Server.init(
    conn_reader.interface(),
    &conn_writer.interface,
);

Note: HTTP client API is still rapidly evolving. For stability-critical code, consider shelling out to curl.

Quick Fixes

| Error | Fix | |-------|-----| | no field 'root_source_file' | Use root_module = b.createModule(.{...}) | | 'std.net' has no member 'Stream' | Networking moved: use std.Io.net.Stream (0.16) | | 'std.net' has no member 'Address' | Use std.Io.net.IpAddress.parse(host, port) (0.16) | | no field 'addIncludePath' in 'Compile' | Methods moved: lib.root_module.addIncludePath(...) (0.16) | | 'timestamp' not found in 'std.time' | Removed: use std.c.clock_gettime(.REALTIME, &ts) (0.16) | | 'Mutex' not found in 'std.Thread' | Removed: use POSIX PthreadMutex shim or std.Io.Mutex (0.16) | | 'random' not found in 'std.crypto' | Removed: use arc4random_buf or std.os.linux.getrandom (0.16) | | 'lockStderrWriter' not found | Renamed: use std.debug.lockStderr(&buf) (0.16) | | local constant shadows declaration | 0.16 forbids local names matching module-level extern fn — rename local | | signed integer division | Use @divTrunc(a, b) not a / b for signed integers (0.16) | | no field 'close' in 'posix' | std.posix.close removed: use _ = std.c.close(fd) (0.16) | | use of undefined value | Arithmetic on undefined is now illegal — initialize explicitly | | type 'f32' cannot represent integer | Use float literal: 123_456_789.0 not 123_456_789 | | ambiguous format string | Use {f} for format methods | | no field 'append' on ArrayList | Pass allocator: list.append(allocator, val) (unmanaged default) | | expected 2 arguments, found 1 on ArrayList | Add allocator param: .append(allocator, val), .deinit(allocator) | | BoundedArray not found | Use std.ArrayList(T).initBuffer(&buf) | | GenericWriter/GenericReader | Use std.Io.Writer/std.Io.Reader | | missing .flush() — no output | Always call try writer.flush() after writing | | enum has no member named 'Struct' | @typeInfo fields now lowercase: .@"struct", .slice, .int | | no field named 'encode' on base64 | Use std.base64.standard.Encoder.encode() | | no field named 'open' on HTTP | Use client.request() or client.fetch() | | expected error union, found Signature | Ed25519.Signature.fromBytes() doesn't return error — remove try | | addSharedLibrary not found | Use b.addLibrary(.{ .linkage = .dynamic, ... }) |

Verification Workflow

After writing or modifying Zig code, verify with this sequence:

  1. zig build — catch compilation errors, match against Quick Fixes above
  2. zig build test — run unit tests
  3. zig build -Doptimize=ReleaseFast test — detect undefined behavior (UB checks enabled in optimized builds)

Development speed tips:

  • zig build --watch -fincremental — incremental compilation, rebuilds on file change
  • 0.15.x uses self-hosted x86_64 backend by default — ~5x faster De

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.