# The Zig Cheatsheet

A dense one-page reference for Zig v0.16.0 — syntax, stdlib,
allocators, comptime and more. Inspired by [cheats.rs](https://cheats.rs/),
with examples cross-referenced to [ziglings](https://codeberg.org/ziglings/exercises).

---
## 01 · Getting Started

Install Zig, write your first program, master the CLI.

### Install & Verify

One static binary, no package manager, nothing else to configure.

Grab a build for your platform from **ziglang.org/download**. Zig ships as a single ~45 MB static binary — no runtime, no VM, no dependencies. Unpack it, put it on your `PATH`, done.

**Verify the install**

```sh
zig version
# 0.16.0
```

- Also available via `brew install zig` / `apt install zig` — but package-manager versions often lag behind the latest release.
- Upgrading is just replacing the folder; projects live in your own directories, not in the toolchain.

### Hello, World! (0.16)
*since 0.16 · ziglings 001–002*

The canonical 0.16 main signature comes with an allocator, I/O handle and CLI args.

**hello.zig**

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

pub fn main(init: std.process.Init) !void {
    try std.Io.File.stdout().writeStreamingAll(init.io, "Hello, world!\n");
}
```

*The 0.16 Juicy Main signature: `init` bundles `init.gpa`, `init.io`, `init.arena` plus CLI args and environment.*

- `std.debug.print("Hello!\n", .{})` writes to **stderr** and works with **any** main signature — ideal for quick prints.
- An empty `pub fn main() !void` is still legal — but then you get no access to CLI args or environment.
- `std.process.Init.Minimal` provides only args + environ, skipping the rest of the I/O setup.

**Compile and run in one step**

```sh
zig run hello.zig
```

### Zig CLI

One tool compiles, tests, formats, cross-compiles — and even builds C code.

| Command | What it does |
| --- | --- |
| `zig run file.zig` | Compile **and** run immediately |
| `zig build-exe` / `-lib` / `-obj` | One-shot compile to executable, library or object file |
| `zig test file.zig` | Build and run every `test` block |
| `zig fmt .` | Canonical formatter — no config, no debates |
| `zig build` | Project build system driven by `build.zig` |
| `zig init` | Scaffold a new project (`build.zig` + `src/main.zig`) |
| `zig fetch <url>` | Fetch and cache a package dependency |
| `zig cc` / `zig c++` | Drop-in C/C++ compiler with free cross-compilation |
| `zig translate-c file.c` | Convert C source to Zig |
| `zig env` / `zig targets` | Show cache/lib paths / list supported compile targets |

| Flag | Effect |
| --- | --- |
| `-O Debug|ReleaseSafe|ReleaseFast|ReleaseSmall` | Choose the build mode (see below) |
| `-target x86_64-linux-musl` | Cross-compile for any `arch-os-abi` |
| `-mcpu <name>` | Select target CPU and feature set |

**Cross-compile a static binary — no extra toolchain**

```sh
zig build-exe main.zig -O ReleaseFast -target x86_64-linux-musl
./main
```

### Build Modes

Four modes trade compile time, runtime speed, size and safety checks.

| Mode | Compile time | Runtime | Safety checks | Notes |
| --- | --- | --- | --- | --- |
| `Debug` | fastest | slowest | all | Default for `zig run`, `zig test` |
| `ReleaseSafe` | slower | optimized | all | Panics on overflow, out-of-bounds, … |
| `ReleaseFast` | slower | fastest | **off** | Violations become undefined behaviour |
| `ReleaseSmall` | slower | small, less optimized | mostly off | Optimizes for binary size |

> ⚠️ **ReleaseFast removes the guard rails:** In `ReleaseFast`, integer overflow or an out-of-bounds access is **undefined behaviour** — it may silently corrupt memory instead of panicking. Ship `ReleaseSafe` unless you truly need the last few percent of performance.

**Typical workflow**

```sh
zig run app.zig                     # iterate in Debug
zig test app.zig                    # tests in Debug
zig build-exe app.zig -O ReleaseSafe  # ship with safety checks
zig build -Doptimize=ReleaseFast      # via build.zig
```

### Comments & Docs

Line comments only — plus two flavours of doc comments for autodoc.

```zig
//! Top-level doc comment: describes the whole FILE.

/// Doc comment for the declaration that follows.
/// Rendered by zig build-docs / autodoc.
pub fn add(a: i32, b: i32) i32 {
    return a + b; // plain line comment
}

// There are no /* block comments */ in Zig.
```

- `//` — the only comment. There are **no block comments**.
- `///` — doc comment; attaches to the declaration below it and shows up in generated docs.
- `//!` — file-level doc comment; must appear at the very top of the file.

### File Anatomy & Imports

Every file is a namespace; `@import` wires them together; `pub` controls visibility.

- `@import("std")` resolves to the standard library; `@import("math.zig")` loads a file **relative to the current file**.
- `pub` makes a declaration visible to importers — everything else stays private to the file.
- Declaration order is irrelevant: top-level decls are lazily analyzed, so forward references just work.
- Every file is implicitly a **struct / namespace**; `@This()` names it from inside.
- The entry point lives in the **root source file** — `main.zig` when you `zig run main.zig`, or the root module declared in `build.zig`.

**main.zig**

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

pub fn main() !void {
    const sum = math.add(2, 3);
    std.debug.print("{d}\n", .{sum});
}
```

**math.zig**

```zig
const magic = 42; // private to this file

pub fn add(a: i32, b: i32) i32 {
    return a + b;
}

// Order never matters:
pub const two = add(1, 1);
```

*Non-`pub` decls like `magic` are invisible to importers — the file boundary is a hard visibility wall.*


---

## 02 · Language Basics

const vs var, statements vs expressions, labeled blocks, `undefined` and the comptime mindset.

### Variables: const & var
*ziglings 003, 051*

Two keywords, strict usage rules, and compile errors that keep code honest.

`const` is immutable, `var` is mutable. Types are usually inferred; write them when it matters: `const x: u32 = 1;`.

```zig
const answer: u32 = 42;      // immutable, explicit type
var counter = answer;        // mutable, type inferred (u32)
counter += 1;                // ok: vars may be reassigned

fn sum(items: []const u32) u32 {
    var total: u32 = 0;
    for (items) |item| total += item;
    return total;
}
```

| Form | Meaning |
| --- | --- |
| `const x = v;` | Immutable binding |
| `var x = v;` | Mutable binding |
| `_ = x;` | Explicitly discard — silences the unused error |
| `_ = &x;` | Take and discard the address — also counts as a use |

> ⚠️ **Compile errors you will hit on day one:** Unused local variables **and parameters** are errors — discard them with `_ = x;`. A `var` that is never mutated is an error. So is **shadowing** an identifier from an outer scope.

### Statements ≠ Expressions

Assignment is a void statement; control flow yields values instead.

Zig separates the two strictly: assignment returns `void`, so `x = y = 0` is impossible. There is no `++` / `--` (use `x += 1`) and no ternary — because `if` **is** an expression.

```zig
const a = 3;
const b = 7;

// const c = a = b;            // ERROR: assignment yields void
// a++;                        // ERROR: no ++ — use a += 1
// const max = a > b ? a : b;  // ERROR: no ternary operator

const max = if (a > b) a else b;         // 7
const ok = (a < b) and (b != 0);         // and / or / ! — never && || !

const both = blk: {                      // blocks are expressions too
    const squared = a * a;
    break :blk squared + b;              // yields 16
};
```

### Blocks & Scope

Labeled blocks are inline scopes that can yield a value.

Declarations live inside their block's scope. Any block can be given a **label**; `break :label value;` exits it with a result — Zig's tool for computed fallback chains and early exits from a scope.

```zig
const n: i32 = -4;

const magnitude = abs: {
    if (n >= 0) break :abs n;   // leave early with a value
    break :abs -n;
};                              // magnitude == 4

// Reads like an inline function:
const squares = first_three: {
    var buf: [3]u32 = undefined;
    for (0..3) |i| buf[i] = @intCast((i + 1) * (i + 1));
    break :first_three buf;
};
```

Labeled blocks replace many nested-`if` and `switch`-fallback patterns — and pair naturally with `errdefer`-style control flow later on.

### undefined
*ziglings 050*

Opt out of initialization — the 0xAA fill and the compiler both have opinions.

`var x: u32 = undefined;` means **no initialization**. In Debug and ReleaseSafe the bytes are filled with `0xAA` so stale reads are obvious; in the other modes they are garbage. Useful for deferred init and buffers you will fully overwrite.

```zig
var buf: [64]u8 = undefined;   // uninit 64 bytes
buf[0] = 'H';
buf[1] = 'i';

var total: u32 = undefined;    // deferred init
total = 10;
total += 5;                    // safe: written before any read
```

> ⚠️ **undefined is not a value:** **Reading** a location that was never written is undefined behaviour — the `0xAA` fill is a debugging aid, not a check. `undefined` may only initialize a `var`; a `const` needs a real value.

### The comptime Keyword

Anything can run at compile time — that is how Zig does generics.

Zig has no macros and no template engine: the language itself is the metaprogramming layer. `comptime` marks parameters, variables or blocks that must be known at compile time — and types themselves are values of type `type`.

```zig
fn Buffer(comptime T: type, comptime size: usize) type {
    return struct {
        items: [size]T,
        len: usize = 0,
    };
}

const Buf8 = Buffer(u8, 8);     // a brand-new type...
const Buf32 = Buffer(u8, 32);   // ...generated at compile time

const n = 40 + 2;               // comptime_int: arbitrary-precision math
```

- `comptime_int` grows arbitrarily — integer literals have no fixed width until assigned to a typed `const` / `var`.
- Every instantiation compiles **fresh code**, including whatever parts of `std` it touches — that is why the whole std library feels like it is compiled per-use.
- This is Zig's answer to C macros and C++ templates: ordinary code, evaluated at compile time.

### Identifiers & Literals
*ziglings 079*

snake_case, raw identifiers for keywords, and every numeric literal prefix.

```zig
const max_value = 1_000_000;     // snake_case — zig fmt enforces the style
const flags = 0b1010;            // binary
const perms = 0o755;             // octal
const mask = 0xFF;               // hex
const big = 1_000.5;             // _ works in floats too

const letter = 'a';              // char literal (comptime_int)
const newline = '\n';
const emoji: u21 = '\u{1F600}'; // unicode code point

const @"if" = 1;                 // raw identifier for keywords...
const @"weird name" = 2;         // ...or any unusual spelling
```

| Literal | Kind |
| --- | --- |
| `123`, `1_000_000` | Decimal integer, `_` as separator |
| `0b1010`, `0o755`, `0xFF` | Binary / octal / hex integer |
| `1_000.5`, `1e10` | Float |
| `'a'`, `'\n'`, `'\u{1F600}'` | Character — a `comptime_int` |
| `@"if"` | Raw identifier — escapes keywords and spaces |


---

## 03 · Primitive Types

Integers of any width, IEEE floats, bool / void / noreturn — and exactly which values coerce.

### Integers
*ziglings 059*

Any bit width you want, plus the special arbitrary-precision comptime_int.

| Type | Notes |
| --- | --- |
| `u8 i8 u16 i16 u32 i32 u64 i64 u128 i128` | The fixed-width family |
| `usize` / `isize` | Pointer-sized; required for indexing |
| `u7`, `i47`, `u65535` | Arbitrary widths — any `uN` / `iN` with N ≤ 65535 |
| `u0` | Zero-bit type; its only value is `0` |
| `comptime_int` | Arbitrary precision — the default type of integer literals |

```zig
const a: u8 = 255;             // fits exactly
const b = 300;                 // comptime_int — no width yet
const c: u16 = b;              // ok: 300 fits u16
// const d: u8 = b;            // compile error: 300 does not fit u8
const e: u9 = 511;             // odd widths are fine
const byte = 'A';              // char literal coerces into u8 and friends
```

### Overflow Behavior

Panics, UB or wraps — plus wrapping and saturating operator variants.

| Build mode | On overflow |
| --- | --- |
| `Debug` / `ReleaseSafe` | Panic (safety-checked) |
| `ReleaseFast` | Undefined behaviour |
| `ReleaseSmall` | Wraps (well-defined) |

| Tool | Meaning |
| --- | --- |
| `+%` `-%` `*%` | Wrapping add / sub / mul |
| `+|` `-|` `*|` | Saturating add / sub / mul |
| `<<|` | Saturating shift left |
| `@addWithOverflow(a, b)` | Returns a tuple: result + overflow bit |

```zig
const big: u8 = 250;

const wrapped = big +% 10;          // 4
const saturated = big +| 10;        // 255
const shifted = @as(u8, 1) <<| 9;   // 255 — saturates instead of UB

const pair = @addWithOverflow(big, @as(u8, 10));
// pair[0] == 4, pair[1] == 1 (overflowed)
```

### Floats
*ziglings 060*

IEEE floats up to 128 bits — conversions stay explicit.

| Type | Notes |
| --- | --- |
| `f16` `f32` `f64` `f80` `f128` | IEEE-754 widths; hardware support varies |
| `comptime_float` | The type of float literals at compile time |

Integers and floats never mix implicitly in arithmetic: `1 + x` with `x: f32` is an error — write `@as(f32, 1) + x`. Convert explicitly with `@floatFromInt`, `@intFromFloat` (out-of-range is undefined behaviour; safety-checked panic in safe modes) and `@floatCast`. There is **no NaN literal** — use `std.math.nan(f64)`.

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

const pi: f64 = 3.14159;
const root = @sqrt(pi);                 // runtime and comptime
const smaller: f32 = @floatCast(pi);    // f64 -> f32
const n: i32 = @intFromFloat(3.99);     // truncates toward zero: 3
const f: f64 = @floatFromInt(7);        // 7.0
const not_a_number = std.math.nan(f64);
```

> ⚠️ **0.16: small integers now coerce into floats:** Since **0.16**, small integer widths coerce into larger float types (e.g. `u8` into `f64`) where every value is exactly representable — no cast needed.

### bool, void & noreturn

Three tiny types that shape control flow.

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

const ready: bool = true;
const check = ready and !ready;   // and / or / ! only

const unit: void = {};            // zero-bit: takes no memory

fn log(msg: []const u8) void {
    std.debug.print("{s}\n", .{msg});
}

fn crash() noreturn {             // never returns
    unreachable;                  // panics — or: while (true) {}
}
```

- `bool` — `true` / `false`, combined with the `and` / `or` / `!` keywords.
- `void` — the unit type; its only value is `{}`. Zero-bit types (`void`, `u0`, empty structs) occupy **no memory**.
- `noreturn` — the type of `unreachable`, panics and infinite loops without `break`; useful as a return type and in `else` prongs.

### Implicit Coercions
*ziglings 061*

Widening is free; narrowing must be explicit and runtime-checked.

| From | To | Implicit? |
| --- | --- | --- |
| `u8` | `u32` (wider, same sign) | **yes** |
| `u32` | `u8` (narrower) | no — `@intCast` |
| `i32` | `u64` (sign change) | no — signs must match |
| `[N]T` | `[]const T` | **yes** |
| string literal `*const [N:0]u8` | `[]const u8` / `[*:0]const u8` / `*const [N]u8` | **yes** |
| `*T` | `?*T` | **yes** (same size) |
| `T` | `?T` | **yes** |
| `T` | `E!T` | **yes** |
| error set | superset error set | **yes** |
| `comptime_int` / `comptime_float` | any int / float that fits | **yes** |

> ⚠️ **Narrowing never happens silently:** `const small: u8 = big;` with `big: u32` is a **compile error**. Make it explicit with `@intCast(big)` — runtime-checked, panics in safe modes when the value does not fit (undefined behaviour in `ReleaseFast`).

### Type Reflection

Introspect any type at compile time via @typeInfo.

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

const T = @TypeOf(42);             // comptime_int
const label = @typeName(u32);      // "u32"
const size = @sizeOf(u64);         // 8 — also @bitSizeOf / @alignOf

const info = @typeInfo(struct { id: u32, name: []const u8 });
const fields = info.@"struct".fields;   // keyword tags need @"" quoting

comptime {
    for (fields) |f| std.debug.print("{s}\n", .{f.name});  // id, name
}
```

- `@typeInfo(T)` returns a **tagged union**; tags are lowercase snake_case: `.int`, `.pointer`, `.@"struct"`, …
- Each struct field exposes `.name`, `.type`, `.default_value` and `.alignment`.
- **0.16:** `@Type` was removed — build types with individual builtins instead: `@Struct`, `@Enum`, `@Pointer`, `@Array`, …


---

## 04 · Arrays, Slices & Strings

Fixed arrays, runtime slices, UTF-8 byte strings, sentinel terminators and SIMD vectors.

### Arrays
*ziglings 004–005*

Fixed length, comptime-known, value semantics.

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

const fixed = [3]u8{ 1, 2, 3 };
const inferred = [_]u8{ 4, 5, 6 };     // length from the initializer
const filled = [1]u8{0xAA} ** 16;      // ** repeats: 16 bytes of 0xAA
const grid = [2][3]u8{
    .{ 1, 2, 3 },                      // nested arrays
    .{ 4, 5, 6 },
};

for (inferred) |item| std.debug.print("{d}\n", .{item});
for (fixed, 0..) |item, i| std.debug.print("[{d}]={d}\n", .{ i, item });
```

- `[N]T` — the length is part of the type and **comptime-known**; `fixed.len` is a compile-time constant.
- Arrays are **values**: assignment copies, and passing to a function copies.
- `for (arr) |item|` iterates values; `for (arr, 0..) |item, i|` adds an index.

### Slices
*ziglings 052–053*

A pointer plus runtime length — the workhorse view type.

```zig
var data = [_]u32{ 1, 2, 3, 4, 5 };

const window: []u32 = data[1..4];      // { 2, 3, 4 }
const rest = data[2..];                // open-ended: to the end
const whole: []const u32 = &data;      // [N]T coerces to []const T

fn sum(items: []const u32) u32 {
    var total: u32 = 0;
    for (items) |item| total += item;  // len lives at runtime
    return total;
}
```

- `[]T` is a mutable view, `[]const T` read-only. A slice is a **pointer + runtime `.len`**; `slice.ptr` hands out the raw pointer.
- Slicing a slice works: `rest[1..2]`. Bounds are **checked at runtime** — out of range panics in safe modes.
- Multi-object loop: `for (as, bs) |a, b|` iterates two sequences in lockstep — equal lengths are a runtime-checked requirement.

### Strings
*ziglings 006–007*

No string type — just []const u8 over UTF-8 bytes.

A string literal has type `*const [N:0]u8` (null-terminated at compile time). The canonical **string type** is `[]const u8` — a byte slice, not null-terminated, holding arbitrary UTF-8. There is no built-in string type and **no concat operator**: build strings with an allocator.

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

const literal = "héllo";                       // *const [6:0]u8
const view: []const u8 = literal;              // canonical string type
const same = std.mem.eql(u8, "abc", "abc");    // byte compare: true
const starts = std.mem.startsWith(u8, "hello!", "hell");
const valid = std.unicode.utf8ValidateSlice("héllo");

// Iterate codepoints:
var it = std.unicode.Utf8View.initUnchecked("héllo").iterator();
while (it.nextCodepoint()) |cp| { _ = cp; }
```

**Concatenation needs an allocator**

```zig
pub fn main(init: std.process.Init) !void {
    const greeting = try std.fmt.allocPrint(init.arena, "hi, {s}!", .{"zig"});
    try std.Io.File.stdout().writeStreamingAll(init.io, greeting);
}
```

| Expression | Type / result |
| --- | --- |
| `"hi"` | `*const [2:0]u8` |
| `"hi"` coerced | `[]const u8`, `[*:0]const u8`, `*const [2]u8` |
| `'x'` | Char literal — a `comptime_int` |
| `std.mem.concat(alloc, u8, .{a, b})` | New heap buffer from parts |

### Multiline String Literals

Every line starts with \\ — no escapes, ever.

```zig
const css =
    \\body {
    \\  color: red;
    \\};
```

- Each line starts with `\\`; line breaks and spacing are preserved **exactly**.
- No escape sequences inside — `\\n` is a literal backslash followed by `n`.
- The final line contributes no trailing newline.

### Sentinel-Terminated
*ziglings 076–078*

Terminators baked into the type — how Zig talks to C.

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

const c_str: [*:0]const u8 = "hello";   // many-item + terminator

var buf: [16:0]u8 = undefined;          // array with a slot for the 0
buf[0] = 'o';
buf[1] = 'k';
buf[2] = 0;

const s: [:0]u8 = buf[0..2 :0];         // slice that keeps the sentinel
const span: []const u8 = std.mem.span(c_str);   // scan to terminator
```

- `[N:0]u8` — sentinel-terminated array; `[:0]u8` — slice whose memory past the end is the sentinel value.
- `std.mem.span` / `std.mem.spanZ` turn a `[*:0]u8` into a length-aware slice — the bridge from C strings.
- `allocator.allocSentinel(u8, n, 0)` allocates a sentinel-terminated buffer.
- Common uses: C interop (`char *`), in-place formatting buffers.

### Vectors (SIMD)
*ziglings 112*

Fixed-width SIMD lanes with element-wise operators.

```zig
const a: @Vector(4, f32) = .{ 1.0, 2.0, 3.0, 4.0 };
const b: @Vector(4, f32) = @splat(4, 2.0);   // 0.14+: length first

const sum = a + b;                 // element-wise: { 3, 4, 5, 6 }
const total = @reduce(.Add, sum);  // horizontal: 18.0
const pick = @select(f32, a > b, a, b);   // per-lane max here
```

- `@Vector(len, T)` — the length must be comptime-known; lanes are bool, integer or float.
- Operators apply **element-wise**; `@reduce` collapses to a scalar; `@select` picks per lane; `std.simd` has extra helpers.
- `@splat(len, scalar)` broadcasts a scalar (length-first signature since **0.14**).

> ⚠️ **0.16: arrays and vectors no longer coerce in memory:** Since **0.16**, `[4]f32` and `@Vector(4, f32)` are kept strictly apart — convert between them explicitly with `@bitCast`.

### Compile-Time Data: @embedFile

Bake files into the binary as comptime-known bytes.

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

const logo = @embedFile("assets/logo.svg");  // *const [N:0]u8
const size = logo.len;                       // comptime-known

pub fn main(init: std.process.Init) !void {
    try std.Io.File.stdout().writeStreamingAll(init.io, logo);
}
```

- The path is relative to the source file and must stay inside the module — the file becomes part of your binary.
- Result type is `*const [N:0]u8`: a comptime-known, sentinel-terminated byte array.
- For **runtime** files, use the file-system APIs (`std.fs`, `Io.Dir`) instead.
- Works naturally with `zig build` modules — assets can also come from dependency packages.


---

## 05 · Pointers

Six pointer spellings, manual arithmetic, casts and alignment discipline — non-null unless you ask.

### Taking Addresses
*ziglings 039–040*

& to take, .* to dereference — parameters are always values.

```zig
var score: u32 = 100;

const p: *u32 = &score;        // pointer to score
p.* += 10;                     // deref-write: score == 110
const q: *const u32 = &score;  // read-only pointer
const v = q.*;                 // deref-read: 110

fn bump(n: *u32) void {        // to mutate the caller's value,
    n.* += 1;                  // take a pointer parameter
}
bump(&score);                  // score == 111
```

Everything is **passed by value** and parameters are immutable — there is no implicit by-reference. To mutate a caller's value (or avoid copying a big struct) pass a `*T`; pass `*const T` for read-only access.

### Pointer Kinds
*ziglings 041–044*

Six pointer spellings — size, length and nullability differ.

| Type | Points to | Length | Nullable | Notes |
| --- | --- | --- | --- | --- |
| `*T` | one item | 1 | no | Non-null, naturally aligned |
| `*const T` | one item | 1 | no | Pointee is read-only |
| `[*]T` | many items | unknown | no | No `.len` — arithmetic allowed |
| `[*:0]T` | many items | sentinel-delimited | no | C-string style |
| `[]T` / `[]const T` | many items | runtime `.len` | no | Slice: pointer + length |
| `[*c]T` | many or one | unknown | yes | C pointer from `translate-c`; coerces both ways |
| `?*T` | one item | 1 | yes | Same size as `*T` — null reuses the zero bits |

A non-optional pointer can **never** be null, and `?*T` costs no extra memory — Zig reserves the all-zero-bits pattern as `null`. Prefer slices over bare pointers whenever a length is meaningful.

### Pointer Arithmetic
*ziglings 054*

Reserved for many-item pointers — slices lend you their `.ptr`.

```zig
var data = [_]u32{ 10, 20, 30, 40 };

const p: [*]u32 = &data;      // many-item pointer
const second = p + 1;         // arithmetic: only on [*]T
second[0] = 99;               // data[1] == 99 — no bounds check

const slice: []u32 = data[0..4];
const base = slice.ptr;       // same [*]u32
const addr: usize = @intFromPtr(base);
const back: [*]u32 = @ptrFromInt(addr);
```

> ⚠️ **Manual, unguarded, yours:** `[*]T` has **no length**, so indexing it skips bounds checks. In most code, slicing (`p[0..n]`) or keeping a `[]T` is the safer — and equally fast — choice.

### Pointer Casts

One builtin per job — and alignment is your responsibility.

```zig
var bytes = [_]u8{ 0x37, 0x13, 0x00, 0x00 };

// Reinterpret the pointee type — here with lowered alignment:
const loose: *align(1) const u32 = @ptrCast(&bytes);

// Raise alignment: you must guarantee it is truly aligned:
const strict: *const u32 = @alignCast(loose);

const frozen: []const u8 = &bytes;
const thawed: []u8 = @constCast(frozen);   // remove const
```

| Builtin | Job |
| --- | --- |
| `@ptrCast` | Change pointee type / pointer width — may only *lower* alignment |
| `@alignCast` | Raise the alignment — UB if the real alignment is lower |
| `@constCast` | Remove `const` from a pointer |
| `@volatileCast` | Remove `volatile` from a pointer |

> ⚠️ **Alignment UB:** Casting a pointer to a **higher** alignment than the data actually has is undefined behaviour — misaligned loads can fault or silently corrupt. `@alignCast` is a promise, not a check.

**0.16:** explicitly aligned pointer types like `*align(1) u32` are now distinct types from the naturally aligned `*u32` — but they still coerce to each other.

### volatile & allowzero

Talking to hardware and the zero address.

```zig
const status_reg: *volatile u32 = @ptrFromInt(0x4000_0000);

// Reads are never elided or reordered away:
while ((status_reg.* & 1) == 0) {}   // wait for the ready bit

// Writes are never removed:
status_reg.* = 0x1;
```

- `*volatile T` marks accesses as **observable** — required for memory-mapped I/O and other sneaky memory.
- `*allowzero T` permits address `0` as a valid pointer value (rare; needed only for special ABIs).
- `@prefetch(ptr, .{})` hints the cache without dereferencing.

### Pointer Metadata & Slice Patterns

Inspect pointer types at comptime; turn buffers into slices early.

```zig
var buf: [64]u8 = undefined;
const n = 10;
const view: []u8 = buf[0..n];    // buffer + count -> slice: the idiom

const info = @typeInfo([]const u8).pointer;
// info.size      -> enum: .one / .many / .slice / .c
// info.is_const  -> bool
// info.alignment -> comptime_int
// info.child     -> u8
// info.sentinel  -> ?u8 (for sentinel pointers)
```

The golden rule: a raw buffer plus a count should become a slice (`buf[0..n]`) as early as possible — slices carry their length, work with `for`, and keep bounds checks.


---

## 06 · Structs

Plain data with namespaces: methods, tuples, destructuring, packed bits and extern layouts.

### Declaring & Initializing
*ziglings 037–038*

Fields, defaults and decl-literal initialization.

```zig
const Point = struct {
    x: f64 = 0,               // default value
    y: f64 = 0,

    const origin = Point{ .x = 0, .y = 0 };   // container-level decl
};

const a: Point = .{ .x = 1, .y = 2 };   // decl-literal init
const b = Point{ .y = 5 };              // full form; x takes the default
const c = a;                            // copy — structs are values
```

- Fields may have **defaults**; omitted fields use them.
- `.{ ... }` is the anonymous init (a decl literal) — it needs a result type; `Point{ ... }` is the explicit form.
- Structs are plain values: assignment and parameter passing **copy**. No inheritance, no constructors — just data plus functions.

### Methods & Namespaces
*ziglings 047–048*

Functions inside a struct — with an explicit self, no magic.

```zig
const Vec = struct {
    x: f64,
    y: f64,

    fn length(self: Vec) f64 {           // by value: read-only use
        return @sqrt(self.x * self.x + self.y * self.y);
    }

    fn scale(self: *Vec, k: f64) void {  // by pointer: mutation
        self.x *= k;
        self.y *= k;
    }
};

var v = Vec{ .x = 3, .y = 4 };
const len = v.length();      // sugar for Vec.length(v)
v.scale(2);                  // sugar for Vec.scale(&v, 2)
```

- There is **no implicit `this`**: the first parameter is the receiver, conventionally named `self` (`Vec`, `*Vec` or `*const Vec`).
- Method-call syntax `v.scale(2)` is sugar — it auto-takes `&v` when the receiver is a pointer.
- Non-method `fn`s and `const`s in the body act as **static** members; `@This()` names the struct itself from inside.

### Tuples & Anonymous Structs
*ziglings 080–083*

Struct literals without a name, plus 0.14 destructuring.

```zig
const pair = .{ 3, 7 };                 // tuple: fields "0", "1"
const point = .{ .x = 1, .y = 2 };      // anonymous struct
const mixed = .{ 1, "two", true };      // heterogeneous tuple
const first = mixed[0];                 // comptime index

const [lo, hi] = pair;                  // destructuring (0.14+)
const .{ .x = px, .y = py } = point;    // by field name

var a: u32 = 1;
var b: u32 = 2;
[a, b] = .{ b, a };                     // swap — no tmp needed
```

- `.{ 1, 2 }` is a **tuple** (fields named `"0"`, `"1"`, …); `.{ .a = 1 }` is an anonymous struct. Both are real struct types.
- Destructuring since **0.14**: `const [a, b] = tup;`, `const .{ .x = x } = point;`, `var` variants, and `_` to discard.
- Tuples power multi-object loops: `for (as, bs) |a, b|` iterates a tuple of slices.
- Without destructuring, a swap needs a temp: `const tmp = a; a = b; b = tmp;`.

### Packed Structs
*ziglings 114–115*

Exact bit layout over a backing integer.

```zig
const Flags = packed struct(u16) {
    visible: bool,      // 1 bit
    layer: u8,          // 8 bits
    tag: u7,            // 7 bits
};                      // 1 + 8 + 7 == 16, no padding

const f = Flags{ .visible = true, .layer = 3, .tag = 127 };
const bits: u16 = @bitCast(f);       // to the backing integer
const back: Flags = @bitCast(bits);  // and back
```

- `packed struct(uN)` lays fields out **bit-exactly** over a backing integer — guaranteed in-memory layout, zero padding.
- Field widths must add up exactly; convert with `@bitCast`.
- **0.16:** pointers are not allowed inside packed structs and packed unions.

**0.16:** packed structs and packed unions can now appear directly as `switch` prong items.

### extern Structs

C-compatible layout for FFI boundaries.

```zig
const Header = extern struct {
    magic: u32,          // field order is guaranteed
    width: u32,
    height: u32,
    data: [*]const u8,   // pointers are fine
};
```

| Kind | Layout | Use for |
| --- | --- | --- |
| `struct` | Compiler's choice — may reorder and pad | Everything inside Zig-land |
| `packed struct(uN)` | Exact bits over a backing integer | Wire formats, hardware registers |
| `extern struct` | C ABI, field order guaranteed | Interop with C, syscalls, file formats |

### Field Introspection

Access fields by name at runtime; walk them at compile time.

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

const User = struct {
    id: u32,
    name: []const u8,
};

const user = User{ .id = 7, .name = "ada" };

comptime {
    std.debug.print("has id: {}\n", .{@hasField(User, "id")});
    std.debug.print("offset of name: {d}\n", .{@offsetOf(User, "name")});

    inline for (std.meta.fields(User)) |f| {
        std.debug.print("{s}\n", .{f.name});   // id, name
    }
}

const picked = @field(user, "name");           // access by runtime name
```

- `@field(obj, "name")` — access a field (or decl) when the name is only known at runtime.
- `@hasField(T, "name")`, `@offsetOf(T, "name")`, and `@fieldParentPtr` for the rare parent-pointer trick.
- `std.meta.fields(T)` (or `@typeInfo(T).@"struct".fields`) + `inline for` is the workhorse for compile-time iteration.


---

## 07 · Enums & Unions

Exhaustive tags, payload unions with compiler-tracked active fields, and packed bit views.

### Enums
*ziglings 035–036*

Tags with an optional integer backing — exhaustive by default.

```zig
const Color = enum { red, green, blue };

const Mode = enum(u8) {        // explicit tag type
    off = 0,
    on = 1,
    auto = 255,
};

const n = @intFromEnum(Mode.on);     // -> u8 value
const m: Mode = @enumFromInt(1);     // checked in safe modes

fn describe(c: Color) []const u8 {
    return switch (c) {              // must be exhaustive
        .red => "stop",
        .green => "go",
        .blue => "chill",
    };
}
```

- Without a backing type, the compiler picks the smallest integer that fits all tags.
- Enums can have methods and decls, just like structs.
- `enum(u8) { a, _ }` declares a **non-exhaustive** enum — switching one requires a `_ =>` prong.
- `@enumFromInt` with a value that has no tag is a checked panic in safe modes; UB in `ReleaseFast`.

### Enum Literals

.red means the red tag — of whichever type the context expects.

```zig
const Color = enum { red, green, blue };
const Team = enum { red, blue };

fn paint(c: Color) void { _ = c; }

paint(.red);                       // .red coerces to Color.red

const t: Team = .red;              // ...and also to Team.red
```

- An enum literal `.red` coerces into **any** enum in the expected type that has a matching tag.
- **0.15:** generalized to **decl literals** — `.empty`, `.none` and friends resolve to any reachable decl, e.g. `var list: std.ArrayList(u8) = .empty;`.
- **0.16:** decl literals may be used directly in `switch` prongs.

### Tagged Unions
*ziglings 056–057*

One payload at a time, with the compiler tracking which.

```zig
const Shape = union(enum) {
    circle: f64,
    rect: struct { w: f64, h: f64 },
    none,
};

fn area(s: Shape) f64 {
    return switch (s) {
        .circle => |r| 3.14159 * r * r,     // capture the payload
        .rect => |r| r.w * r.h,
        .none => 0,
    };
}

const s: Shape = .{ .circle = 2.0 };        // init one field
const empty = s == .none;                   // tag check: false
```

- `union(enum)` gives every payload a tag; `switch` must handle **all** of them (exhaustive).
- Capture payloads in prongs: `.circle => |r|` by value, `.rect => |*r|` for mutation.
- Field-less members like `none` carry no data.
- `std.meta.activeTag(s)` reads the active tag explicitly; unions are value types.

### Untagged & extern Unions
*ziglings 055*

Raw field aliasing — you own the active-field bookkeeping.

```zig
const Value = union {
    int: i32,
    float: f64,      // same storage — fields alias
};

var v: Value = .{ .int = 42 };
v.float = 3.5;       // overwrites the same bytes
```

> ⚠️ **No tag, no mercy:** Nothing tracks which field is active. **Reading a field you did not last write is undefined behaviour** — the compiler assumes the active field and may optimize accordingly. Use a tagged `union(enum)` unless an ABI forces otherwise.

`extern union` gives C-compatible overlay layout for FFI (think C's `union`); plain `union` is the in-Zig variant. Both expect **you** to remember the active field.

### Packed Unions

Field aliasing at the bit level over a backing integer.

```zig
const Reg = packed union(u2) {
    raw: u2,
    parts: packed struct(u2) { low: bool, high: bool },
};

const r: Reg = .{ .raw = 0b10 };

// 0.16+: packed unions can be compared for equality:
const matches = r == .{ .parts = .{ .low = false, .high = true } };
```

- `packed union(uN)` overlays its fields bit-exactly on a backing integer — the classic register-view tool.
- **0.16:** packed unions support `==` comparisons and can appear as `switch` prong items.

### Choosing: enum vs union vs struct

A ten-second decision table.

| If you need... | Reach for | Data |
| --- | --- | --- |
| One of N states | `enum` | none — just the tag |
| One of N states, each with data | `union(enum)` | exactly one active payload |
| Overlapping views of the same bits | `union` / `packed union` / `extern union` | you track the active field |
| All fields at once | `struct` | everything, always |

A tagged union is Zig's safe version of C's tagged-struct idiom and C++'s `std::variant`: the tag is compiler-managed, and `switch` refuses to compile until every case is handled.


---

## 08 · Optionals

Null without the billion-dollar mistake — `?T` is a value or `null`, and every unwrap is compiler-checked.

### Optional Basics
*ziglings 045–046, 050*

`?T` is a value or `null` — test it, default it, or capture it; there is no truthiness.

`?T` holds a value of `T` or `null`. Test with `o == null`, default with `orelse`, unwrap-assert with `.?`, or capture the payload with `if (o) |v|`.

**Create, test, unwrap**

```zig
const maybe: ?i32 = null;
const n: ?i32 = 42;

if (n) |v| {
    std.debug.print("got {d}\n", .{v});  // payload capture
} else {
    std.debug.print("nothing\n", .{});
}

const a = maybe orelse 0;        // 0 — fallback when null
const b = n.?;                   // 42 — panics if null
std.debug.print("{d} {d}\n", .{ a, b });
```

| Operation | Value present | `null` |
| --- | --- | --- |
| `o orelse d` | yields `o` | yields `d` |
| `o.?` | yields `o` | **panic**: attempt to use null value |
| `if (o) |v| a else b` | `a`, with `v` bound | `b` |
| `while (o) |v| { ... }` | body per value | loop ends |

### While-Unwrap & Iterators
*ziglings 045–046, 050*

`while (iter.next()) |item|` is the idiomatic iterator loop; `else` fires when the loop ends without `break`.

An iterator is any type with `fn next() ?T` — each call yields the next value or `null`. The `while`-unwrap loop consumes it: the body runs per value, the loop ends when `null` arrives.

**Custom iterator over ?u32**

```zig
const Counter = struct {
    remaining: u32 = 3,
    fn next(self: *Counter) ?u32 {
        if (self.remaining == 0) return null;
        self.remaining -= 1;
        return self.remaining;        // yields 2, 1, 0
    }
};
var it = Counter{};
while (it.next()) |item| {            // ?u32 unwrapped per pass
    std.debug.print("{d} ", .{item});
} else {
    std.debug.print("ended, no break\n", .{}); // runs: no break used
}
```

The `else` clause — on `while` and `for` alike — runs only when the loop ended **without** `break`; for error unions it takes the last error: `else |e|`. `for (slice, 0..) |item, i|` walks items with their index, and multi-sequence `for (as, bs) |a, b|` zips sequences pairwise.

### Optional Pointers
*ziglings 045–046, 050*

`?*T` costs nothing extra — null hides in the pointer's spare bit pattern; `?u32` pays for a tag.

`?*T` occupies exactly the same bytes as `*T`: address `0` is never a valid pointer, so it encodes `null` for free. Payloads without a spare bit pattern (`?u32`, `?bool`) need a hidden tag byte. This is why linked-list `next` fields, tree children, and `find`-style APIs returning `?usize` all use optional pointers.

```zig
const Entry = struct {
    id: u32,
    next: ?*Entry = null,       // linked list: null is free
};

const Config = struct {
    home: ?[]const u8 = null,   // optional slice field
    fallback: ?*const Config = null,
};

fn findIndex(hay: []const u8, needle: u8) ?usize {
    for (hay, 0..) |c, i| {
        if (c == needle) return i;   // find-style API
    }
    return null;
}
```

| Type | Size | Why |
| --- | --- | --- |
| `?*T` | == `@sizeOf(*T)` | `0` is reserved — `null` is free |
| `?*const T` | == `@sizeOf(*const T)` | same trick |
| `?u32` | > `@sizeOf(u32)` | extra tag, padded to alignment |
| `?bool` | > `@sizeOf(bool)` | no spare bit pattern |

### Patterns & Gotchas
*ziglings 045–046, 050*

orelse chains, nested optionals, and why `catch` is not `orelse`.

```zig
// orelse chain: first non-null wins
const home = env_home orelse env_user orelse "/tmp";

// capture payload BY POINTER: mutate in place, no copy
if (map.getPtr(key)) |ptr| ptr.count += 1;

// nested optional ??T: legal but rare — unwrap twice
const deep: ??u32 = n;
if (deep) |inner_opt| {
    if (inner_opt) |v| std.debug.print("{d}\n", .{v});
}
```

- `null` has its own singleton type — `@TypeOf(null)`; optionals are not booleans
- `a orelse b orelse c` short-circuits to the first non-null
- nested `??T` is allowed but almost always a design smell — prefer `?T` or an error union
- `catch` belongs to **error unions**, `orelse` to **optionals** — the two do not interchange

> ⚠️ `x.?` panics with **attempt to use null value** — and that safety check only exists in Debug and ReleaseSafe. In ReleaseFast it is compiled out: undefined behavior. Prefer `orelse` or `if`-capture unless the invariant is locally provable.


---

## 09 · Error Handling

Errors are ordinary values: `E!T` return types, `try`/`catch`, `errdefer` — no exceptions, no unwinding.

### Error Sets
*ziglings 021–025, 033*

`error{...}` defines a set of error values — checked at compile time, returned, never thrown.

An error set — `error{FileNotFound, OutOfMemory}` — is a distinct set of error **values**, not a class hierarchy. Values coerce upward into any superset for free; `anyerror` accepts every error that exists.

```zig
const OpenError = error{ FileNotFound, AccessDenied };

// combine sets: merged superset, coercion is automatic
const IoError = OpenError || error{ EndOfStream };

// anyerror: the global set — erases the concrete set
var last: anyerror = error.OutOfMemory;

fn open() OpenError!void {
    return error.FileNotFound;   // must be in the return set
}
```

| Form | Meaning |
| --- | --- |
| `error{A, B}` | set literal; values are `error.A`, `error.B` |
| `E1 || E2` | merged superset (coercion target) |
| `anyerror` | the global set of all errors |
| `E!T` | error union: an `E` value or a `T` payload |

Error sets are **not exceptions**. They are return values: an error must be handled at the call site or propagated explicitly, and nothing ever unwinds.

### Error Unions & Inferred Sets
*ziglings 021–025, 033*

`E!T` is error or value; `!T` lets the compiler infer the set; `try`/`catch` unwrap it.

`E!T` is a union: an error value or a payload. `!T` (no set named) means **inferred** — the compiler collects every error the body can return, and inferred sets compose across call chains. Bind one explicitly as `IoError!u32` when you want a stable contract.

```zig
fn parse(s: []const u8) !u32 {            // ! = inferred error set
    if (s.len == 0) return error.Empty;   // raise: return an error
    return try std.fmt.parseInt(u32, s, 10);
}

const v = parse("42") catch 0;            // fallback value
const w = parse("42") catch unreachable;  // asserts no error
const x = parse("nope") catch |e| blk: {  // labeled catch
    std.debug.print("failed: {s}\n", .{@errorName(e)});
    break :blk 0;
};
```

**try**

```zig
const v = try parse(s);
```

**catch |e| return e**

```zig
const v = parse(s) catch |e| return e;
```

*`try f()` is exactly this sugar: unwrap, or return the error from the enclosing function.*

| Form | On error | On success |
| --- | --- | --- |
| `try f();` | returns the error from this fn | yields the value |
| `f() catch 0;` | yields `0` | yields the value |
| `f() catch unreachable;` | panics | yields the value |
| `f() catch |e| handler;` | runs handler, `e` in scope | yields the value |

### Handling Payloads
*ziglings 021–025, 033*

Unwrap error unions with `if`/`while` payload capture; switch on the error value itself.

```zig
const r: ReadError!u32 = read();

if (r) |v| {                          // success: v is the payload
    std.debug.print("ok {d}\n", .{v});
} else |e| {                          // error: e is the error value
    switch (e) {                      // switch on the error set
        error.FileNotFound => create(),
        else => return e,             // propagate the rest
    }
}

while (tryConnect()) |conn| {         // body runs on each success
    serve(conn);
} else |e| return e;                  // runs on the final error
```

There is no `switch` on an error union — unwrap with `if (r) |v| ... else |e| ...` first, then `switch` the bare error value. A `switch (e)` over a set must be exhaustive or end in `else`.

- `@errorName(e)` returns the name as `[]const u8` — the builtin is `@errorName`, there is no `std.error` module
- `if (r) |v| { ... } else |e| { ... }` — success and error branches in one statement
- `while (attempt()) |v| { ... } else |e| { ... }` — retry loops for free

### errdefer
*ziglings 021–025, 033*

`errdefer` runs cleanup only when an error return passes through its scope.

`errdefer` arms when execution passes it and fires only when an **error return** leaves the scope — never on success. Arming it before the resource's own `try` is pointless: that `try` already bailed out of the function.

**Resource cleanup ordering (LIFO)**

```zig
const conn = try connect(gpa);   // acquire
errdefer conn.close();           // undo ONLY if we return an error

const buf = try gpa.alloc(u8, 64);
errdefer gpa.free(buf);          // LIFO: freed before conn closes

errdefer |e| std.log.warn("failed: {s}", .{@errorName(e)});

try conn.handshake();            // any failure here or below:
return conn.read();              // log, free buf, close conn, propagate
```

| Form | Fires |
| --- | --- |
| `defer f();` | every scope exit |
| `errdefer f();` | scope exit via error return only |
| `errdefer |e| f(e);` | same, with the error value bound |
| success return after arming | nothing — the errdefer is skipped |

### Panics & Unreachable
*ziglings 021–025, 033*

`@panic` aborts with no unwinding; `unreachable` is a contract the optimizer relies on.

```zig
std.debug.assert(x > 0);           // "assertion failed" in safe modes

if (buf.len > max_len) @panic("buffer overrun by design");

fn step(s: Phase) void {
    switch (s) {
        .start, .running => advance(s),
        .done => unreachable,       // invariant: never called when done
    }
}
```

- `@panic("msg")` prints message + trace and aborts — **no unwinding: defers do not run**
- `unreachable` — `reached unreachable code` panic in Debug/ReleaseSafe; undefined behavior in ReleaseFast
- `std.debug.assert(cond)` — panics in Debug/ReleaseSafe, compiled out in ReleaseFast/ReleaseSmall
- error return traces print in safe builds — every `try` the error crossed, like a reverse stack trace
- gate debug-only code with `builtin.mode == .Debug` (`const builtin = @import("builtin");`)

> ⚠️ **No exceptions, no catch-alls.** An error is handled where it occurs or returned explicitly — and error unions cannot cross `extern`/`export` boundaries: convert to C-style return codes at the FFI seam.

### Error Style Guide
*ziglings 021–025, 033*

Errors carry no payload — keep sets small, add context where it exists, `try` liberally.

Error values carry no payload or message. The idiomatic response: small per-module sets, context added at the call site that has it, and `errdefer` for cleanup.

- no payloads, no wrapper types — attach context by logging where you have it, plus `errdefer`
- keep error sets small and per-module; `error{OutOfMemory}` is ubiquitous and coerces everywhere
- `try` liberally; handle at the call site that actually has context to recover
- don't use optionals for failures — `?T` means *absent*, `E!T` means *went wrong*
- returning an error from `main` prints the error trace and exits nonzero

```zig
fn load(gpa: Allocator, path: []const u8) !Config {
    const file = try openConfig(path);   // low-level error, no context
    defer file.close();

    return parseConfig(gpa, file) catch |e| {
        std.log.err("config {s}: {s}", .{ path, @errorName(e) }); // context here
        return e;                        // propagate the original error
    };
}
```


---

## 10 · Control Flow

Conditions must be real `bool`; `if`, loops, and `switch` are expressions — plus labeled blocks for structured jumps.

### if / else if / else
*ziglings 009–017, 030–031, 062–063, 098, 103–104, 111*

Conditions must be genuine `bool`; `if` is an expression — that's why there is no ternary.

No truthiness: `if (ptr)` and `if (n)` are compile errors — compare explicitly. As an expression, every branch must yield the same type, which is exactly what the ternary would do.

```zig
const x: i32 = 7;

// statement form: condition must be bool
if (x > 5) std.debug.print("big\n", .{});

// expression form: replaces the ternary
const parity: []const u8 = if (x % 2 == 0) "even" else "odd";

// payload sugar (see Optionals / Error Handling)
if (maybe) |v| use(v);                     // optional
if (result) |v| use(v) else |e| fail(e);   // error union
```

- braces are optional for a single statement; `zig fmt` keeps whatever you wrote
- when used as an expression, all branches must yield one common type
- `if (o) |v|` unwraps an optional; `if (r) |v| ... else |e|` unwraps an error union

### while
*ziglings 009–017, 030–031, 062–063, 098, 103–104, 111*

`while` with a continue-expression, labeled `break`, and an `else` clause — no `do-while`.

`while (cond) { }` repeats until the condition is false. The continue-expression `: (i += 1)` runs after every pass — including after `continue` — making it the safest place for increments.

```zig
var i: u32 = 0;
while (i < 5) : (i += 1) {    // continue-expr runs after each pass
    if (i == 3) continue;
    if (i == 4) break;
}

// no do-while: use while (true) + break
while (true) {
    if (ready()) break;
}

// else: runs when the condition ends the loop WITHOUT a break
while (poll()) |v| {          // optional / error-union unwrap
    if (v == 0) break;
} else flush();               // poll() ran dry
```

- `break`/`continue` can target a label: `break :outer`, `continue :outer`
- `else` after `while`: fires when the loop ended without `break` (also `else |e|` for error unions)
- no `do-while` — write `while (true)` plus a `break`

### for & Ranges
*ziglings 009–017, 030–031, 062–063, 098, 103–104, 111*

`for` walks exclusive ranges and slices, zipping sequences with an optional index.

`for (0..n) |i|` iterates the exclusive range `0..n`. Sequences zip by position — `for (xs, ys, 0..) |x, y, i|` — and all lengths must match (runtime check in safe builds).

```zig
for (0..3) |i| {                      // exclusive range: 0, 1, 2
    std.debug.print("{d} ", .{i});
}
const xs = [_]u8{ 10, 20, 30 };
const ys = [_]u8{ 1, 2, 3 };

for (xs, ys, 0..) |x, y, i| {         // zip: item, item, index
    std.debug.print("{d}+{d}@{d} ", .{ x, y, i });
}                                     // equal lengths, runtime-checked

for (xs) |x| {
    if (x == 20) continue;
    if (x == 30) break;
} else std.debug.print("no break\n", .{});
```

- `_` discards a slot: `for (xs, 0..) |_, i|` iterates indices only
- no reverse ranges — index backwards by hand, or `std.mem.reverse` a copy first
- mutate in place with `for (slice) |*item| item.* += 1`
- `else` on `for` runs when the loop finished without `break`

### switch
*ziglings 009–017, 030–031, 062–063, 098, 103–104, 111*

Exhaustive by default; ranges with `...`; prongs capture tagged-union payloads.

`switch` is exhaustive by default: cover every value or add `else`. It works as expression or statement, and captures `|v|` bind payloads — especially for tagged unions.

```zig
const label = switch (n) {          // expression: prongs yield a value
    0 => "zero",
    1, 2, 3 => "small",             // value list
    4...8 => "medium",              // inclusive range
    else => "large",
};

fn area(s: Shape) f32 {             // union(enum): capture payloads
    return switch (s) {
        .circle => |r| r * r * 3.14159,
        .rect => |d| d[0] * d[1],
    };                              // exhaustive: no else needed
}
```

| Prong | Matches |
| --- | --- |
| `1 => ...` | a single value |
| `1, 2 => ...` | a list of values |
| `1...5 => ...` | an inclusive range |
| `.circle => |r| ...` | payload capture on `union(enum)` |
| `else => ...` | everything unmatched |

- prongs are comma-separated — trailing comma when they span lines
- switch works on ints, bools, enums, error sets, and `union(enum)`s — not strings or floats (use `if/else` + `std.mem.eql`)
- 0.16: prongs accept packed struct/union items (`.{ .b = 3 }`) and decl literals as values

### Labeled Blocks & Loops
*ziglings 009–017, 030–031, 062–063, 098, 103–104, 111*

Labels turn nested loops and blocks into structured `goto` — and blocks can yield values.

A label names a loop or block so `break :label` / `continue :label` can target it from any nesting depth. Labeled blocks double as multi-step expressions.

```zig
outer: for (rows) |row| {
    for (row) |cell| {
        if (cell < 0) break :outer;     // exits BOTH loops
        if (cell == 0) continue :outer; // next outer iteration
    }
}

const idx = blk: {                      // labeled block yields a value
    for (names, 0..) |name, i| {
        if (std.mem.eql(u8, name, want)) break :blk i;
    }
    break :blk null;                    // not found
};
```

- labels work on blocks, `for`, and `while` alike
- `continue :label` restarts the labeled loop's next iteration
- `break :blk value` makes a block an expression — replaces `goto` and early-multi-break acrobatics

### unreachable
*ziglings 009–017, 030–031, 062–063, 098, 103–104, 111*

Proves impossibility to the compiler — or costs you UB in optimized builds.

`unreachable` asserts a line can never execute. The compiler may optimize based on that promise. Safe modes verify it at runtime: panic **reached unreachable code**. ReleaseFast believes you: undefined behavior.

```zig
fn drive(s: State) void {
    switch (s) {
        .parked, .driving => move(s),
        .scrapped => unreachable,   // invariant: can't happen here
    }
}
```

> ⚠️ Never validate input with `unreachable` — hostile data hitting it in ReleaseFast is memory corruption, not a crash. Use `@panic`, `try`, or `else => return error.X` for anything reachable.


---

## 11 · Functions

Required return types, immutable params, no closures — functions are plain, predictable declarations.

### Declaring Functions
*ziglings 018–020, 032*

Return type required, params immutable, no defaults — and `pub` controls visibility.

The return type is REQUIRED — use `void` when there is nothing to return. Parameters are immutable bindings, and there are no default or named arguments.

```zig
/// Adds two integers.
pub fn add(a: i32, b: i32) i32 {
    return a + b;
}

pub fn logHello(name: []const u8) void {  // void: no return value
    std.debug.print("hello {s}\n", .{name});
    // params are const: assigning to name is a compile error
}
```

- `pub` exposes a decl to `@import`ers of this file — private otherwise
- parameters are `const`; to change one, copy into a local first
- `///` doc comments attach to decls and appear in autodoc
- no overloading — use distinct names or comptime dispatch

### Multiple Return Values
*ziglings 018–020, 032*

No multi-return sugar — return a struct/tuple, an optional, an error union, or use out-params.

Zig has no multi-value return sugar — pick a shape: `!T` for failure, `?T` for absence, a struct or anonymous tuple for several values, `*T` out-params for mutation.

```zig
fn divMod(a: u32, b: u32) struct { q: u32, r: u32 } {
    return .{ .q = a / b, .r = a % b };
}

const .{ .q = q, .r = r } = divMod(17, 5);   // 3 and 2 (0.14+)
std.debug.print("{d} {d}\n", .{ q, r });

// alternatives: ?T (absent), E!T (failure), *T (out-param)
fn indexOf(s: []const u8, c: u8) ?usize {
    for (s, 0..) |ch, i| if (ch == c) return i;
    return null;
}
```

- tuple return type: `struct { u32, u32 }` — anonymous, destructured by position
- destructuring since 0.14: `const .{ .q = q, .r = r } = ...;`
- `_` discards a destructured slot you don't need

### Passing Arguments
*ziglings 018–020, 032*

Pass-by-value with compiler-optimized lowering — pointers only for mutation or sharing.

Arguments pass by value; the compiler picks the cheap lowering, so don't contort signatures over sizes. Reach for pointers when you must mutate the caller's data or share it — and remember params are immutable, so `*T` is the only way to write back.

```zig
fn byValue(p: Point) Point {         // copy — fine for small data
    return .{ .x = p.x * 2, .y = p.y * 2 };
}

fn scale(p: *Point, f: f32) void {   // mutate the caller's value
    p.x *= f;
    p.y *= f;
}

fn total(items: []const u32) u32 {   // read-only view, any length
    var sum: u32 = 0;
    for (items) |v| sum += v;
    return sum;
}
```

| Situation | Signature |
| --- | --- |
| read-only, small data | pass `T` by value |
| read-only, big data | `[]const T` or `*const T` |
| mutate caller's data | `*T` |
| accept an array | param `[]const T` — `[N]T` coerces |
| optional argument | `?T` explicitly (no default args) |

### export, extern & callconv
*ziglings 018–020, 032*

`export` publishes C-visible symbols, `extern` declares them, `callconv` pins the ABI.

The calling convention decides the ABI. `export`/`extern` bridge Zig to C and the linker; `inline` is a codegen hint that forces call-site expansion.

```zig
export fn zig_callback(x: i32) i32 {   // C-visible symbol
    return x * 2;
}

extern "c" fn printf(fmt: [*:0]const u8, ...) c_int; // C variadic

fn cStyle(x: u32) callconv(.c) u32 {   // explicit C ABI (0.14+)
    return x + 1;
}

inline fn square(x: i32) i32 {         // body copied into callsites
    return x * x;
}
```

- `export fn f()` emits the symbol for C/linkers; `extern` declares one defined elsewhere
- `extern "c"` names the library; variadic `...` requires the C calling convention
- `callconv(.c)` on a normal fn pins the C ABI (lowercase since 0.14)
- `callconv(.naked)` for asm-only stubs — no prologue or epilogue
- `inline fn` forces inlining at each call site

### Function Pointers
*ziglings 018–020, 032*

`*const fn (...) T` types, `&f` spelling, no closures — context goes through a parameter.

A function pointer's type is `*const fn (i32, i32) i32`. Take a function's address with `&f` — the safest spelling — then call it like a function. There are no closures: pass captured state through an explicit context parameter.

```zig
fn add(a: i32, b: i32) i32 { return a + b; }
fn sub(a: i32, b: i32) i32 { return a - b; }

const Op = *const fn (i32, i32) i32;

fn apply(f: Op, a: i32, b: i32) i32 {
    return f(a, b);
}

const f: Op = &add;                    // &fn is the canonical spelling
std.debug.print("{d} {d}\n", .{ apply(f, 3, 4), apply(&sub, 3, 4) });
```

- used for callbacks, interrupt vectors, and hand-rolled vtables (see comptime & Generics for the zero-cost alternative)
- capture state C-style: `fn cb(ctx: *anyopaque, x: u32) void` with the context as the first parameter
- comptime fn parameters specialize per function: `fn wrap(comptime f: fn () void)`

### Recursion
*ziglings 018–020, 032*

Runtime recursion just works (until the stack); comptime recursion needs an eval-branch budget.

Runtime recursion is plain stack recursion — nothing special, nothing detected. Comptime recursion is bounded by the eval branch quota, raised with `@setEvalBranchQuota`.

```zig
fn fib(n: u32) u64 {                  // runtime: plain stack recursion
    return if (n < 2) n else fib(n - 1) + fib(n - 2);
}

fn Fib(comptime n: u32) u64 {         // comptime: quota-bounded
    return if (n < 2) n else Fib(n - 1) + Fib(n - 2);
}

comptime {
    @setEvalBranchQuota(1_000_000);   // default quota is only 1000
    _ = Fib(25);                      // evaluated during compilation
}
```

- infinite runtime recursion dies with a stack overflow — not detected, no tail-call guarantee
- recursive generic instantiation can explode compile times — memoize or flatten
- `@setEvalBranchQuota` raises the comptime step budget for the current evaluation


---

## 12 · defer & Cleanup

Scope-exit cleanup: `defer`, `errdefer`, and the acquire/release discipline that replaces RAII.

### defer
*ziglings 027–029*

Runs at the END OF SCOPE — the block, not the function — always, in LIFO order.

`defer` schedules a statement for the end of its **enclosing block** — not the function. Sibling defers run LIFO (last registered, first run) on every exit path: return, error return, `break`.

**Mutex + heap example**

```zig
fn demo(gpa: Allocator) !void {
    const data = try gpa.alloc(u8, 128);
    defer gpa.free(data);           // paired right at acquisition

    lock.lock();
    defer lock.unlock();            // LIFO: runs BEFORE free

    // ... work — early returns, errors, breaks: all safe
}
```

> ⚠️ Defers do **not** run on panic or abort — the process dies on the spot. One more reason panics are for bugs and errors are for expectations.

### errdefer
*ziglings 027–029*

Same LIFO machinery, but only on the failure path — combine for commit/rollback.

`errdefer` fires only when an error return passes through its scope — never on success. The classic combo: `errdefer rollback` right after acquiring, `commit` as the last step. Two lines make a transaction.

```zig
const conn = try connect(gpa);
errdefer conn.close();          // only on error return
try conn.handshake();           // bail? -> closed for you

try tx.begin();
errdefer tx.rollback();         // failure path
try tx.write(records);
tx.commit();                    // success path — keep it last
```

| Exit path | `defer` | `errdefer` |
| --- | --- | --- |
| success `return` | runs | skipped |
| error `return` / failed `try` | runs | runs |
| leave the scope via `break` | runs | skipped |
| panic / abort | skipped | skipped |

### Scope Rules & Gotchas
*ziglings 027–029*

defer is block-scoped: in a loop body it fires every iteration — or holds resources the whole loop.

defer binds to the nearest enclosing block. In a loop body that is one iteration; inside an `if` block, that block; at function top, the function.

```zig
fn scan(gpa: Allocator, paths: []const []const u8) !void {
    for (paths) |path| {
        const f = try open(path);            // acquire per iteration
        defer f.close();                     // released per iteration

        const buf = try gpa.alloc(u8, 1024); // also per iteration
        defer gpa.free(buf);                 // LIFO in this body:
        try process(f, buf);                 // free buf, then close f
    }
}
```

- defer reads variables at RUN time, not at registration — `defer log(i)` sees `i` as it is when the scope exits
- want the value as it was at defer-time? snapshot first: `const j = i; defer use(j);`
- leaving the scope via `break` still runs defers — only panic/abort skips them

> ⚠️ A defer **outside** a loop body holds the resource for the whole loop. If each iteration should own the resource, move the acquire/defer pair into the body.

### Common Cleanup Table
*ziglings 027–029*

acquire → defer release, immediately, before any `try` in between.

The discipline that makes defer work: every acquire is IMMEDIATELY followed by its release deferral — before any `try`, `return`, or second acquire can slip between them.

| Acquire | Release | Notes |
| --- | --- | --- |
| `gpa.alloc(u8, n)` | `gpa.free(mem)` | same allocator, same scope |
| open a file | `f.close()` | whatever opened it closes it |
| `lock.lock()` | `lock.unlock()` | `std.Thread.Mutex` |
| acquire a refcount | release it | balance every +1 with a -1 |
| `tx.begin()` | `tx.commit()` | `errdefer tx.rollback()` |
| suspend | resume | keep the pair visually adjacent |

```zig
const mem = try gpa.alloc(u8, size);
defer gpa.free(mem);             // <- no try between these two lines

const out = try dest.begin();    // next resource
defer out.finish();              // paired before any risk
```


---

## 13 · comptime & Generics

Run arbitrary code at compile time — generics are just functions on `type`, with no macro preprocessor in sight.

### comptime Fundamentals
*ziglings 064–075, 084*

`comptime` marks code the compiler executes at build time — params, vars, and blocks.

`comptime` marks code the compiler runs at build time: parameters, variables, whole blocks. All generics, `type` values, and default values are comptime — this one mechanism replaces C macros, templates, and pre-build codegen.

```zig
fn repeat(comptime n: u32, msg: []const u8) void {
    comptime var i: u32 = 0;              // comptime variable
    inline while (i < n) : (i += 1) {     // unrolled at compile time
        std.debug.print("{s} ", .{msg});
    }
}

const big = 1_000_000_000_000;   // comptime_int: unbounded, exact
const small: u8 = 200;           // coerced to u8 at compile time
```

- `comptime_int` / `comptime_float`: unbounded, exact literal types — they coerce into concrete types on use
- `if (comptime cond)` evaluates now and prunes the untaken branch from the binary
- a `comptime` parameter creates a distinct instantiation per value

### Generic Functions
*ziglings 064–075, 084*

`type` is a comptime value — take it as a parameter and the fn gets monomorphized per call.

A `type` parameter makes the function generic — `T` is just a comptime value. Every distinct `T` produces a fresh monomorphized copy of the body; there is no runtime polymorphism involved.

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

const a = max(i32, 3, 9);        // explicit type argument
const b = max(u8, 1, 2);         // each T = a fresh instantiation

fn sum(items: anytype) i64 {     // anytype: inferred param type
    var total: i64 = 0;
    for (items) |x| total += x;
    return total;
}
```

- `anytype`: infer the parameter type from the call site
- `@hasDecl(T, "name")` / `@hasField(T, "name")` — duck-type checks on capabilities
- constraints are implicit — misuse fails where used; add `@compileError` for clear messages

### Generic Types (type-returning fns)
*ziglings 064–075, 084*

A function returning `type` is a generic type; `@This()` names the struct from inside.

Generic types are functions that return `type`. The struct declared inside is a fresh type for every `T`; identical arguments always yield the identical type.

**Canonical generic container**

```zig
fn List(comptime T: type) type {
    return struct {
        items: []T,
        len: usize = 0,

        const Self = @This();    // name this struct from inside

        fn push(self: *Self, item: T) void {
            self.items[self.len] = item;
            self.len += 1;
        }
    };
}

const IntList = List(i32);       // fresh type, monomorphized
```

- `@This()` — the innermost type's own name, for `Self`-style aliases and methods
- methods close over the generic parameter — no extra plumbing
- instantiate like any decl: `const IntList = List(i32);`

### inline for / inline while
*ziglings 064–075, 084*

Unroll loops at comptime — one stamped-out copy per iteration.

`inline for` / `inline while` force unrolling: the body is stamped out once per iteration at compile time. Required whenever each iteration must generate distinct code from comptime values.

```zig
const Point = struct { x: u32 = 0, y: u32 = 0 };

fn zeroAll(p: *Point) void {
    inline for (@typeInfo(Point).@"struct".fields) |field| {
        @field(p, field.name) = 0;    // x, then y — unrolled
    }
}
```

- `field.name` is a comptime string, and `@field(obj, name)` needs a comptime-known name — a runtime loop can't supply either
- plain loops stay runtime loops when nothing per-iteration is comptime — inline loops bloat binaries
- `inline while` unrolls the same way

### Type-Creating Builtins (0.16)
*since 0.16 · ziglings 064–075, 084*

`@Type` is gone in 0.16 — construct types with focused builtins instead.

`@Type` was removed in 0.16 — constructing types now goes through focused builtins, one per kind. Reach for them only when the type cannot be spelled directly.

| Builtin | Builds | Sketch |
| --- | --- | --- |
| `@Int` | integer type | `@Int(signedness, bits)` |
| `@Float` | float type | `@Float(bits)` |
| `@Pointer` | pointer type | size / child / alignment args |
| `@Array` | array type | `@Array(len, child)` |
| `@Vector` | SIMD vector | `@Vector(len, child)` |
| `@Tuple` | tuple type | `@Tuple(&.{ u8, u16 })` |
| `@Struct` | struct type | fields arg — see docs |
| `@Union` | union type | see docs |
| `@Enum` | enum type | see docs |
| `@Opaque` | opaque type | see docs |
| `@Optional` | optional type | `@Optional(u32)` → `?u32` |
| `@ErrorUnion` | error union | set + payload args |
| `@ErrorSet` | error set | names arg — see docs |
| `@Fn` | function type | see docs |
| `@EnumLiteral` | enum literal type | the type of `.tag` |

> ⚠️ These shipped with `@Type`'s removal in **0.16** and signatures are still settling — check the std docs for your exact version. For most code, spelling types directly (`?u32`, `[4]u8`, `*T`) beats constructing them.

### Metaprogramming Toolkit
*ziglings 064–075, 084*

Reflect, generate, and fail fast — the everyday comptime toolbox.

Reflection and generation in one toolbox — everything here is evaluated during the build, never at runtime.

| Tool | What it does |
| --- | --- |
| `@typeInfo(T)` | reflect — tags like `.int`, `.@"struct"`, `.pointer`, `.error_union` |
| `@field(obj, "name")` | access a field or decl by comptime name |
| `@hasField(T, "name")` | does the struct have this field? |
| `@typeName(T)` | human-readable type name |
| `@sizeOf(T)` / `@offsetOf(T, "f")` | layout facts |
| `@call(.auto, f, .{args})` | call with a comptime-built argument tuple |
| `@compileError("msg")` | fail compilation with your message |
| `@compileLog(...)` | print values during compilation — remove before shipping |
| `@setEvalBranchQuota(n)` | raise the comptime step budget (default 1000) |
| `@embedFile(path)` | file contents as `*const [n:0]u8` at comptime |
| `std.meta.fields(T)` | field list as a comptime slice |
| `usingnamespace` | **removed since 0.15** — no replacement; declare and import explicitly |

**Comptime type validation**

```zig
fn checkPairs(comptime T: type) void {
    inline for (@typeInfo(T).@"struct".fields) |f| {
        if (@sizeOf(f.type) == 0) {
            @compileError("zero-sized field: " ++ f.name);
        }
    }
}

const Bad = struct { ok: u32, ghost: u0 };
comptime checkPairs(Bad);   // compile error: zero-sized field: ghost
```

### Duck-Typed Interfaces (no vtables)
*ziglings 084*

Interfaces are comptime checks — or explicit fn-pointer structs when the type is runtime-chosen.

**Compile-time interface check (ziglings 084 pattern)**

```zig
fn describe(comptime T: type, thing: T) void {
    comptime {
        if (!@hasDecl(T, "describe"))
            @compileError(@typeName(T) ++ " needs a describe() decl");
    }
    thing.describe();          // duck typing, resolved at compile time
}

const Cat = struct {
    fn describe(self: Cat) void {
        std.debug.print("meow\n", .{});
    }
};
```

When the concrete type is chosen at **runtime**, comptime duck typing is out. Build a vtable by hand — a struct of function pointers plus a context pointer. That is exactly how `std.Io.Writer` / `std.Io.Reader` dispatch since **0.15**.

**Hand-rolled vtable**

```zig
const Speaker = struct {
    ctx: *anyopaque,
    speakFn: *const fn (*anyopaque) void,

    fn speak(self: Speaker) void {
        self.speakFn(self.ctx);      // runtime dispatch
    }
};
```

- comptime interfaces: zero cost, monomorphized — but the type must be known at compile time
- vtable interfaces: one copy of code, runtime dispatch — works with runtime-chosen types
- `*anyopaque` context + `@ptrCast` back is the standard state-capture idiom (there are no closures)


---

## 14 · Memory & Allocators

Explicit allocation: no GC, no hidden malloc.

### The Allocator Interface
*ziglings 099*

Every allocation goes through a passed-in `std.mem.Allocator` — nothing allocates behind your back.

Zig has **no hidden allocations**. Anything that needs memory takes a `std.mem.Allocator` — by convention it is the **first parameter**. The allocator is a small vtable interface, so the caller picks the strategy (arena, debug, fixed buffer, C, ...).

| allocate | release | returns |
| --- | --- | --- |
| `create(T)` | `destroy(ptr)` | `!*T` |
| `alloc(T, n)` | `free(slice)` | `![]T` |
| `allocSentinel(T, n, 0)` | `free(slice)` | `![:n]T` |
| `dupe(T, slice)` / `dupeZ(T, slice)` | `free` | `![]T` / `![:0]T` |
| `realloc(slice, n)` | `free` | `![]T` |
| `remap(slice, n)` | `free` | `?[]T` — may **move** |
| `resize(slice, n)` | — | `bool` — in-place only |

All allocating operations return errors (`!`) — out of memory is an ordinary error value, never a panic. The caller owns the result and must free it (or hand the job to an arena).

**Allocator as first parameter**

```zig
fn readName(gpa: std.mem.Allocator) ![]u8 {
    const buf = try gpa.alloc(u8, 32);
    defer gpa.free(buf);
    buf[0] = 'z';
    return try gpa.dupe(u8, buf[0..1]); // caller frees this copy
}
```

### ArenaAllocator

Allocate freely, free everything at once — the workhorse for parsers, CLIs and request handling.

An arena hands out allocations from big chunks it grabs from a child allocator. Individual `free` is not needed (or possible for single items) — **everything is released at once** by `deinit()`.

**Scratch memory for one operation**

```zig
fn handle(gpa: std.mem.Allocator) ![]u8 {
    var arena = std.heap.ArenaAllocator.init(gpa);
    defer arena.deinit(); // frees everything allocated below
    const a = arena.allocator();

    const tmp = try a.alloc(u8, 64);
    const msg = try std.fmt.allocPrint(a, "parsed {d} tokens", .{3});
    // ... use tmp and msg freely - no per-slice frees ...
    return try gpa.dupe(u8, msg); // only the result escapes
}
```

- `.init(child)` — wrap any allocator; `.allocator()` — get the interface; `.deinit()` — free all
- `reset(.retain_capacity)` — keep the memory, drop the allocations: reuse the same arena between phases (per frame, per request, per iteration)
- Ideal for **CLIs, parsers, request handling**: one arena per unit of work
- Since **0.16** the arena is thread-safe and lock-free

### DebugAllocator (ex-GPA)
*since 0.16*

`GeneralPurposeAllocator` was renamed `std.heap.DebugAllocator` in 0.16 — leak and double-free detection for development builds.

**The standard development setup**

```zig
var gpa: std.heap.DebugAllocator(.{}) = .init(.{});
defer std.debug.assert(gpa.deinit() == .ok); // .ok == no leaks
const allocator = gpa.allocator();

const n = try allocator.create(u32);
n.* = 5;
defer allocator.destroy(n);
```

`GeneralPurposeAllocator` was renamed `std.heap.DebugAllocator` since **0.16**. In Debug builds it tracks allocations and reports **leaks and double-frees** with stack traces; `gpa.deinit()` returns `.ok` when nothing leaked.

| allocator | reach for it when |
| --- | --- |
| `std.heap.DebugAllocator(.{})` | default in development: leak / double-free checks |
| `std.heap.smp_allocator` | ReleaseFast multi-threaded programs (0.16) |
| `std.heap.ArenaAllocator` | many small allocations, freed all at once |
| `std.heap.c_allocator` | your program links libc anyway |
| `std.heap.page_allocator` | raw OS pages, no bookkeeping, coarse |
| `std.heap.FixedBufferAllocator` | no-heap targets, embedded, bounded scratch |

### Lifetime Rules

No GC: freed memory stays freed. Ownership is a convention you enforce with discipline and tooling.

- Freed memory stays freed — every `alloc` needs exactly one `free` (or an arena `deinit`)
- Use-after-free and double-free are **UB**; `DebugAllocator` turns them into clear panics in Debug
- Slices from an allocator are only valid while the allocator lives — keep it alive as long as the data
- `resize` returns `bool` and only succeeds in place; `remap` may **move** — always use the returned slice, old pointers can dangle after a grow/shrink
- Never free stack memory through an allocator; never return a slice that points into a local buffer

> ⚠️ **UB in one line:** Using a slice after `free` (or after arena `deinit` / `reset`) is undefined behavior. In Debug builds `std.heap.DebugAllocator` catches it; in ReleaseFast it is silent corruption.

**remap may move your memory**

```zig
const p = try gpa.alloc(u8, 8);
const q = gpa.remap(p, 16) orelse return error.OutOfMemory;
// p may be invalid now - use q from here on
@memset(q, 0);
gpa.free(q);
```

### Patterns & Pitfalls

Arena per request, allocator as first parameter, and what to do about `deinit()` results.

- **Arena per request**: one arena per unit of work, `reset(.retain_capacity)` between iterations
- **Pass the allocator down**: functions take `gpa: std.mem.Allocator` as first parameter — no global heap
- **Store an allocator in a struct** only if the object owns its allocations; prefer taking it per call (unmanaged containers do exactly this since 0.15)
- `_ = gpa.deinit();` — discard the leak-report result when you intentionally leak at process exit
- Test for leaks with `std.testing.allocator` — it fails the test on any unfreed memory (see **Testing**)

**Arena inside, allocator passed down**

```zig
const Parser = struct {
    gpa: std.mem.Allocator,

    fn parse(self: *Parser) !void {
        var arena = std.heap.ArenaAllocator.init(self.gpa);
        defer arena.deinit(); // scratch memory freed in one shot
        const tmp = try arena.allocator().alloc(u8, 64);
        _ = tmp;
        // long-lived results allocate from self.gpa instead
    }
};
```


---

## 15 · Standard Library

Formatting, containers, hashing, files, processes — and the 0.15/0.16 API shake-up.

### Printing & Writers (0.16)
*since 0.16 · ziglings 002*

`std.debug.print` for quick stderr output; stdout goes through the `std.Io` writer interface.

`std.debug.print("...", .{args})` writes formatted text to **stderr**, cannot fail, and works in every build mode and target. For **stdout** you go through the `std.Io` interface.

**stderr vs stdout**

```zig
// stderr, formatted - the everyday printf:
std.debug.print("x={d} name={s}\n", .{ x, name });

// stdout, raw bytes (0.16, from "Juicy main"):
try std.Io.File.stdout().writeStreamingAll(init.io, "hello\n");

// buffered formatted stdout (0.15+ shape):
var buffer: [1024]u8 = undefined;
var w = std.Io.File.stdout().writer(&buffer);
try w.interface.print("count={d}\n", .{ n });
try w.interface.flush();
```

*The 0.15/0.16 writer signatures are still settling — see the std docs for your exact version.*

| specifier | meaning |
| --- | --- |
| `{s}` | string / bytes |
| `{d}` | int or float, decimal |
| `{c}` | character |
| `{x}` / `{X}` | hexadecimal, lowercase / uppercase |
| `{b}` | binary |
| `{o}` | octal |
| `{e}` | scientific notation |
| `{?}` | optional of a formattable value |
| `{any}` | anything (debug formatting) |
| `{f}` | call the type's own `.format` method (since 0.15) |
| `{{` / `}}` | literal braces |
| `{d:0>4}` | width 4, fill `0`, align right |
| `{s:^10}` | width 10, centered |

- Arguments are passed as an anonymous tuple: `.{ x, name }`
- `std.debug.print` returns `void` (best-effort, no error handling needed)
- Writer-based printing returns errors — `try` it and `flush()` at the end

### Building Strings
*ziglings 106*

`allocPrint` for one-shots, `std.mem.concat` for joins, `Writer.Allocating` for incremental building.

**One-shot and concat**

```zig
const s = try std.fmt.allocPrint(gpa, "a={d} b={s}", .{ a, b });
defer gpa.free(s);

const joined = try std.mem.concat(gpa, u8, &.{ "foo", "bar" });
defer gpa.free(joined);
```

**Incremental: Writer.Allocating (0.16)**

```zig
var aw: std.Io.Writer.Allocating = .init(gpa);
defer aw.deinit();
try aw.writer.print("n={d}\n", .{ n }); // .writer is a FIELD, not a method
const owned = try aw.toOwnedSlice(); // take over the buffer
```

*`aw.written()` gives a **borrowed** view of what was written so far; `toOwnedSlice()` transfers ownership instead.*

Rule of thumb: one formatted string → `allocPrint`; appending in a loop → `Writer.Allocating` or an `ArrayList(u8)` (see **ArrayList**).

### ArrayList
*since 0.15 · ziglings 102*

Growable array: unmanaged since 0.15 (allocator per call), `.empty` initialization since 0.16.

`std.ArrayList(T)` is a growable array. Since **0.15** only the **unmanaged** form exists — the allocator is passed to each method. Since **0.16** you initialize with `.empty`.

**0.16 style**

```zig
var list: std.ArrayList(u32) = .empty;
defer list.deinit(gpa);
try list.append(gpa, 1);
try list.appendSlice(gpa, &.{ 2, 3 });
list.items[0] = 9;
const owned = try list.toOwnedSlice(gpa);
```

| method | notes |
| --- | --- |
| `append(gpa, v)` / `appendSlice(gpa, s)` | add at the end, `!void` |
| `insert(gpa, i, v)` / `insertSlice(gpa, i, s)` | shift right, `!void` |
| `pop()` | returns `?T` since 0.15 — `null` when empty |
| `orderedRemove(i)` | returns `T`, keeps order, O(n) |
| `swapRemove(i)` | returns `T`, swaps in last element, O(1) |
| `items` / `items.len` | the underlying slice / element count |
| `clearRetainingCapacity()` | len = 0, memory kept for reuse |
| `resize(gpa, n)` / `ensureTotalCapacity(gpa, n)` | grow (`!void`) |
| `toOwnedSlice(gpa)` | hand over the exact-sized buffer |
| `deinit(gpa)` | free the buffer |

> ⚠️ Do **not** hold `*T` pointers into `list.items` across `append` — growth may reallocate and invalidate them (see **Pointer Pitfalls**).

### HashMaps

`AutoHashMap` / `StringHashMap` (hashed) and the ArrayHashMap family (insertion order preserved).

`std.AutoHashMap(K, V)` derives hashing for any hashable `K` (ints, packed structs, ...); `std.StringHashMap(V)` hashes `[]const u8` keys. **ArrayHashMap** variants additionally keep insertion order.

**Managed map (allocator stored)**

```zig
var map: std.StringHashMap(u32) = .init(gpa);
defer map.deinit();
try map.put("a", 1);
if (map.get("a")) |n| std.debug.print("a={d}\n", .{ n }); // ?u32
if (map.getPtr("a")) |ptr| ptr.* += 1; // mutate in place
var it = map.iterator();
while (it.next()) |e|
    std.debug.print("{s}={d}\n", .{ e.key_ptr.*, e.value_ptr.* });
```

| type | notes |
| --- | --- |
| `std.AutoHashMap(K, V)` | auto-hashed keys |
| `std.StringHashMap(V)` | `[]const u8` keys |
| `std.AutoArrayHashMap(K, V)` | + insertion order, `.keys()` / `.values()` |
| `std.StringArrayHashMap(V)` | string keys + order |
| `std.array_hash_map.Auto` / `.String` / `.Custom` | since 0.16: ArrayHashMap family moved here (managed removed) |

- `*Unmanaged` variants take the allocator per call (like all containers since 0.15)
- `get` returns `?V`; `getPtr` returns `?*V` for in-place mutation; `getOrPut` inserts-if-absent
- Iterators yield `key_ptr` / `value_ptr` — dereference with `.*`
- ArrayHashMap variants preserve **insertion order**; plain HashMap does not

### Sorting & Searching

`std.mem.sort` (stable block sort) and `std.mem.sortUnstable` (pdq) with a context-first comparator.

**Sort structs by a field, descending**

```zig
const Score = struct { name: []const u8, score: u32 };

fn byScoreDesc(_: void, a: Score, b: Score) bool {
    return a.score > b.score; // context comes FIRST (0.15+)
}

fn demo(items: []Score) void {
    std.mem.sort(Score, items, {}, byScoreDesc); // stable (block sort)
    std.mem.sortUnstable(Score, items, {}, byScoreDesc); // pdq, faster
}
```

| function | notes |
| --- | --- |
| `std.mem.sort(T, items, ctx, lessThan)` | stable, in-place, O(n log n) |
| `std.mem.sortUnstable(...)` | pdq sort, not stable, usually faster |
| `std.sort.binarySearch(T, items, ctx, compareFn)` | requires sorted items — compare-callback shape varies, see std docs for your version |

`lessThan` has signature `fn (ctx, lhs, rhs) bool` — return `lhs < rhs` for ascending order. Return `>` for descending as above.

### std.mem Helpers
*ziglings 109–110*

Compare, trim, split, find — with the 0.16 renames (`trimStart`, `find*`, new `cut*`).

| helper | notes |
| --- | --- |
| `eql` / `eqlIgnoreCase` | slice equality |
| `startsWith` / `endsWith` | prefix / suffix test |
| `trim` / `trimStart` / `trimEnd` | cut chars from both/start/end (renamed from trimLeft/trimRight since 0.16) |
| `find` / `findLast` | indexOf* renamed since 0.16: `indexOf` → `find`, `lastIndexOf` → `findLast` |
| `cut*` | split at the **first** match — new helpers since 0.16 |
| `splitScalar` / `splitSequence` / `tokenizeScalar` | lazy iterator over parts |
| `replaceOwned` / `join` | build new strings (allocate) |
| `min` / `max` | element-wise min/max of slices |
| `zeroes(T)` | a zeroed `T` value (comptime-known) |

**Everyday std.mem + std.fmt**

```zig
const t = std.mem.trim(u8, "  hi \t", " \t"); // "hi"
if (std.mem.startsWith(u8, t, "hi")) { /* ... */ }

const n = try std.fmt.parseInt(u32, "4040", 10); // !u32
const f = try std.fmt.parseFloat(f64, "3.5"); // !f64
```

`splitScalar`/`tokenizeScalar` return iterators — `while (it.next()) |part| { ... }`. `tokenize` skips empty parts, `split` yields them.

### Files & Dirs (std.Io)
*since 0.16*

Since 0.16 the file system lives behind the `std.Io` interface — every operation takes an `io` handle.

Since **0.16** the file system is part of the `std.Io` interface: `std.Io.Dir` and `std.Io.File` operations take an `io` handle (obtained from `main(init)`).

**Read and write a file**

```zig
// read, bounded to 1 MiB:
const bytes = try std.Io.Dir.cwd().readFileAlloc(
    io, "input.txt", gpa, .limited(1 << 20),
);
defer gpa.free(bytes);

// write:
var f = try std.Io.Dir.cwd().createFile(io, "out.txt", .{});
defer f.close(io);
try f.writeStreamingAll(io, "hi\n");
```

*0.16 API — exact signatures may shift in patch releases; consult the std docs for your version.*

- `std.Io.Dir.cwd()` — current directory handle; `openDir(io, path, .{})` for others
- Directory iteration and recursive `walk` (selective walking since 0.16)
- **Preopens** (WASI-style, since 0.16): directories are capabilities handed to the program
- `std.Io.File.stdout()` / `.stderr()` — standard streams

### Process, Args, Env, Time & Random (0.16)
*since 0.16*

All process-level services are non-global since 0.16 — they arrive via `main(init: std.process.Init)`.

Since **0.16** args, environ, spawning, time and randomness are **non-global**: they are capabilities passed to `pub fn main(init: std.process.Init)` ("Juicy main"). An empty `pub fn main()` is still legal.

**Args and environ from init**

```zig
pub fn main(init: std.process.Init) !void {
    const args = try init.minimal.args.toSlice(init.arena.allocator());
    const user = init.environ_map.get("USER") orelse "nobody";
    std.debug.print("{s}: {d} args\n", .{ user, args.len });
}
```

**Running a child process**

```zig
const result = try std.process.run(gpa, io, .{ .argv = &.{ "git", "status" } });
std.debug.print("{s}", .{result.stdout});
```

| service | 0.16 access |
| --- | --- |
| args | `init.minimal.args.toSlice(init.arena.allocator())` |
| environment | `init.environ_map.get("KEY")` (`.keys()` / `.values()`) |
| children | `std.process.run(gpa, io, .{ .argv = ... })` |
| time | `std.Io.Clock` / `Timestamp` / `Duration` types — `Duration` formats with `{f}` |
| randomness | moved to the `std.Io` interface since 0.16 — see the io docs |

For args+environ only, `std.process.Init.Minimal` is the lightweight variant of the init struct.


---

## 16 · Testing

Built-in test framework: no dependencies, leak detection by default, fuzzing in the toolchain.

### Test Blocks
*ziglings 105*

Any `test "..." { ... }` block runs under `zig test` — assertions come from `std.testing`.

**A test next to the code it tests**

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

fn add(a: i32, b: i32) i32 {
    return a + b;
}

test "add works" {
    try testing.expect(add(2, 2) == 4);
    try testing.expectEqual(@as(i32, 4), add(2, 2));
    try testing.expectFmt("4", "{d}", .{add(2, 2)});
}
```

Tests live **next to the code** — in the same file, or in a separate test file that imports it. `zig test math.zig` finds and runs every `test` block. Assertions return `!void`, so prefix them with `try`.

| helper | checks |
| --- | --- |
| `expect(cond)` | condition is true |
| `expectEqual(expected, actual)` | deep equality (structs, arrays) |
| `expectEqualStrings(a, b)` | string content + length |
| `expectEqualSlices(T, a, b)` | element-wise slice equality |
| `expectError(err, expr)` | expr fails with exactly `err` |
| `expectApproxEqAbs` / `Relative` | float comparison with tolerance |
| `expectFmt(expected, fmt, args)` | value formats to expected text |

Note that unreferenced functions are not analyzed — a test that never calls `f` will not type-check `f`'s body. Use `refAllDecls` (next entry).

### Running & Organizing

`zig test` for files, `zig build test` for projects, `refAllDecls` to force analysis.

**A test root that pulls in everything**

```zig
// src/all.zig
test {
    std.testing.refAllDecls(@This()); // force analysis of all decls
    _ = @import("parser.zig"); // and other files' tests
    _ = @import("json.zig");
}
```

- `zig test math.zig` — run one file's tests
- `zig test src/all.zig` — run a project via an aggregating root file
- `zig build test` — through a test step in build.zig (see **Test Step**)
- `zig test --test-filter "json" src/all.zig` — only matching test names
- Name tests descriptively: `test "json: parses empty object"`

On failure the default runner prints the expression, expected vs actual, and a stack trace. Add `--summary all` (with `zig build`) for a per-step breakdown.

### Allocator-Aware Tests
*ziglings 105*

`std.testing.allocator` detects leaks per test — a leak fails the test with a stack trace.

**A leak fails the test**

```zig
test "leaks fail the test" {
    const a = std.testing.allocator;
    const buf = try a.alloc(u8, 16);
    _ = buf;
    // oops: no "defer a.free(buf);"
    // -> FAILS with a leak report (address + stack trace)
}
```

**Arena for scratch data in tests**

```zig
test "arena for scratch" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const a = arena.allocator();
    const s = try std.fmt.allocPrint(a, "{d}", .{42});
    try std.testing.expectEqualStrings("42", s);
}
```

`std.testing.allocator` is a leak-checking wrapper around `DebugAllocator`. `expectEqual` compares **deeply** for structs, unions and arrays — no per-field assertions needed.

### Fuzzing

Fuzz targets are ordinary tests — `zig build test --fuzz` runs them coverage-guided.

**A fuzz target (see std.testing.fuzz for your version)**

```zig
test "parser never crashes on garbage" {
    const ctx = {};
    std.testing.fuzz(ctx, struct {
        fn f(_: @TypeOf(ctx), input: []const u8) void {
            _ = parse(input) catch {}; // any crash = fuzz failure
        }
    }.f, .{});
}
```

*The `std.testing.fuzz` API is still evolving — check the std docs for your version.*

- `zig build test --fuzz` — coverage-guided fuzzing (since 0.14)
- Fuzz targets are **normal test blocks**; safe build modes turn memory errors into catchable crashes
- Run it locally against your parser / decoder / any `[]const u8` consumer

- 0.16 fuzzing improvements include:
- AST smith — grammar-aware structured input generation
- multiprocess fuzzing
- infinite mode
- crash dumps for reproduction


---

## 17 · Build System

build.zig is Zig: declarative steps, cross-compilation and package management built in.

### Minimal build.zig (0.16)
*since 0.16*

Target + optimize + one executable, installed with `zig build`, run with `zig build run`.

**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 = "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);
    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);
}
```

Modern style (0.15+) nests `.root_source_file`, `.target` and `.optimize` inside `root_module = b.createModule(...)`; setting them directly on the executable options is the legacy shape.

- `zig build` — default step: compile and install to `zig-out/bin`
- `zig build run` — run the `run` step defined above
- `--release=fast` — optimized build without editing anything

### Steps, Options & Artifacts

Custom `-D` options, named steps, and the artifact kinds the build graph understands.

**Options and steps**

```zig
const level = b.option(u32, "level", "compression level 0-9") orelse 6;
const target = b.standardTargetOptions(.{}); // -Dtarget, -Dcpu
const optimize = b.standardOptimizeOption(.{}); // --release=...
const t = b.step("test", "Run tests"); // zig build test
```

| invocation | effect |
| --- | --- |
| `zig build` | default step: install artifacts |
| `zig build run` | run a custom step |
| `zig build test --summary all` | test step + full failure summary |
| `--release=safe|fast|small` | optimize mode (Debug if unset) |
| `-Doptimize=ReleaseSafe` | same, via the standard option |
| `-Dtarget=x86_64-windows-gnu` | cross-compile target triple |
| `-Dlevel=9` | your own `b.option` values |
| `--prefix ./out` | install directory instead of `zig-out` |

- Artifacts: `b.addExecutable(...)` / `b.addLibrary(...)` / `b.addObject(...)`
- `b.installArtifact(artifact)` attaches it to the default install step
- `b.addRunArtifact(...)` runs executables **and** tests as a build step
- `b.default_step` — what plain `zig build` runs

### Modules & Dependencies

Wire multi-file projects with `createModule` + imports; fetch packages via build.zig.zon.

**Two modules, one import**

```zig
const lib_mod = b.createModule(.{
    .root_source_file = b.path("src/lib.zig"),
    .target = target,
    .optimize = optimize,
});
const exe_mod = b.createModule(.{
    .root_source_file = b.path("src/main.zig"),
    .target = target,
    .optimize = optimize,
    .imports = &.{
        .{ .name = "lib", .module = lib_mod },
    },
});
// in main.zig:  const lib = @import("lib");
```

**Package dependency from build.zig.zon**

```zig
// build.zig.zon:
//   .dependencies = .{ .lib = .{ .url = "https://...", .hash = "..." } },
const dep = b.dependency("lib", .{});
exe.root_module.addImport("lib", dep.module("lib"));
// add deps with:  zig fetch --save <url-or-tarball>
```

| zon field | notes |
| --- | --- |
| `.name` | enum literal since 0.14: `.my_project` |
| `.version` | semver string, e.g. `"0.1.0"` |
| `.fingerprint` | package identity hash (0.14+) |
| `.minimum_zig_version` | oldest Zig that can build this |
| `.dependencies` | map of `{ .url, .hash }` (or local path) deps |
| `.paths` | files/dirs included when the package is used |

### Linking & C Files

linkLibC, system libraries, include paths, .c sources — and translate-c for headers.

**Mixing C into a Zig build**

```zig
exe.linkLibC(); // -lc
exe.linkSystemLibrary("sdl2"); // -lSDL2
exe.addIncludePath(b.path("include")); // -I
exe.addCSourceFiles(.{
    .files = &.{ "vendor/foo.c", "vendor/bar.c" },
    .flags = &.{},
});
```

**translate-c for headers (0.16)**

```zig
const tc = b.addTranslateC(.{
    .root_source_file = b.path("src/bindings.h"),
    .target = target,
    .optimize = optimize,
});
exe.root_module.addImport("c", tc.createModule());
```

*`@cImport` is deprecated since **0.16** — this build-based flow replaces it (see **C Interop**).*

### Test Step

The standard `zig build test` recipe: addTest + addRunArtifact + a named step.

**Standard test step**

```zig
const tests = b.addTest(.{ .root_module = mod });
const run_tests = b.addRunArtifact(tests);
const t = b.step("test", "Run unit tests");
t.dependOn(&run_tests.step);
```

- `addTest` analyzes every `test` block reachable from the module root
- Force analysis of leaf decls with `std.testing.refAllDecls` in your code (see **Testing**)
- Run: `zig build test`; add `--summary all` for details
- `zig build test --fuzz` — fuzz mode (see **Fuzzing**)
- Unit-test **timeouts** are available as an option since 0.16

The test step depends on the same module graph as your exe — tests compile with the target and optimize settings of the module they test.


---

## 18 · C Interop

Call C, export Zig, translate headers, and cross-compile C itself — no glue layer needed.

### C Types & [*c] Pointers
*ziglings 096–097*

`c_int` and friends for C-width integers; `[*c]T` for nullable C pointers of unknown length.

| Zig type | C equivalent |
| --- | --- |
| `c_char`, `c_short`, `c_int`, `c_long`, `c_longlong` | `char`, `short`, `int`, `long`, `long long` |
| `c_uint`, `c_ushort`, `c_ulong`, `c_ulonglong` | unsigned variants |
| `c_longdouble` | `long double` |
| `bool` | `bool` (C99 `_Bool`) |
| `usize` | `size_t` |
| `f32` / `f64` | `float` / `double` (fixed width) |

`[*c]T` is the **C pointer**: nullable, `allowzero`, coerces in both directions with `[*]T`, `[*:0]T` and `?[*]T`, and has **unknown length** (no indexing, no `.len`). A null check works through optional coercion: `if (cptr) |p| ...`.

**C pointers in practice**

```zig
extern fn strlen(s: [*c]const u8) usize;

fn safeLen(s: [*c]const u8) usize {
    if (s) |p| return strlen(p); // null check via optional coercion
    return 0;
}

// sentinel-terminated C string -> slice:
//   const slice: []const u8 = std.mem.span(cstr);
```

`std.mem.span(cstr)` converts a `[*:0]const u8` into a `[]const u8` by counting to the sentinel — the bridge between C strings and Zig slices.

### Calling C Functions
*ziglings 096–097*

Declare `extern` fns yourself, link libc, done — headers optional when you import a translate-c module.

**extern declarations**

```zig
extern "c" fn puts(s: [*:0]const u8) c_int;
extern fn abs(x: c_int) c_int; // "c" implied when linking libc
extern "c" fn printf(fmt: [*:0]const u8, ...) c_int; // variadic

pub fn main() void {
    _ = puts("hello from zig");
    _ = abs(-42);
    _ = printf("n=%d\n", @as(c_int, 7));
}
```

- Library name optional: `extern fn ...` defaults to the C ABI of the linked libc
- Variadic: `...` in the parameter list, like C
- Functions passed **into** C use the C calling convention: `fn f() callconv(.c) void`

**Linking**

```zig
// build.zig:
exe.linkLibC();
exe.linkSystemLibrary("m"); // libm, SDL2, ... anything pkg-config finds

// command line:
//   zig build-exe main.zig -lc
```

### Exporting Zig for C

`export fn` emits C-ABI symbols; `extern struct`/`union`/`enum` fix the ABI layout; callbacks via `callconv(.c)`.

**Exported functions and symbol control**

```zig
export fn add(a: c_int, b: c_int) c_int {
    return a + b; // symbol "add" lands in the object file
}

fn mul(a: c_int, b: c_int) callconv(.c) c_int {
    return a * b;
}

comptime {
    @export(&mul, .{ .name = "my_mul" }); // custom symbol name
}
```

`extern struct`, `extern union` and `extern enum(c_int)` guarantee C ABI field layout — use them for types shared across the boundary. Plain Zig structs may reorder/pad differently.

**Passing a Zig callback into C**

```zig
const Callback = *const fn (ctx: ?*anyopaque, n: c_int) callconv(.c) void;
extern "c" fn register(cb: Callback) void;

fn onEvent(ctx: ?*anyopaque, n: c_int) callconv(.c) void {
    _ = ctx;
    std.debug.print("event {d}\n", .{n});
}

pub fn init() void {
    register(onEvent);
}
```

- `zig build-lib lib.zig` — static library
- `zig build-lib -dynamic lib.zig` — shared library
- The C header for your exports must be written by hand

### Headers: translate-c (0.16)
*since 0.16*

`@cImport` is deprecated — translate headers in build.zig and import them as a module.

**0.15 and earlier — @cImport (deprecated)**

```zig
const c = @cImport({
    @cInclude("sqlite3.h");
});

const rc = c.sqlite3_open(":memory:", &db);
```

**0.16 — build-based translate-c**

```zig
// build.zig:
const tc = b.addTranslateC(.{
    .root_source_file = b.path("vendor/sqlite3.h"),
    .target = target,
    .optimize = optimize,
});
exe.root_module.addImport("c", tc.createModule());

// main.zig:
// const c = @import("c");
```

*Same result — but the 0.16 way is explicit in the build graph and caches per header.*

- One-shot CLI: `zig translate-c vendor/sqlite3.h > bindings.zig`, then `@import("bindings.zig")`
- Include paths and defines can be given on the translate-c step
- Vendoring the generated `bindings.zig` avoids a build step entirely

> ⚠️ `@cImport` still compiles in 0.16 but is **deprecated** — migrate new code to `b.addTranslateC` now.

### zig cc — C/C++ Toolchain

Zig ships a full clang-based C/C++ compiler with cross-compilation and bundled libc targets.

**sh**

```sh
zig cc main.c -o main              # drop-in clang/gcc replacement
zig cc -target aarch64-linux-gnu main.c -o main  # instant cross-compile
zig c++ main.cpp -o main           # C++ too
```

- libc headers and libraries are **bundled**: musl, glibc, mingw-w64 — no system toolchain required
- Same target-triple syntax as `zig build -Dtarget=...`
- Ideal for building C dependencies and Makefile-free builds
- Flags are clang-compatible: `-I`, `-L`, `-l`, `-static`, ...

**sh**

```sh
# build a C library once, for two targets, no Docker:
zig cc -c -Iinclude vendor/foo.c -target x86_64-linux-gnu -o foo-linux.o
zig cc -c -Iinclude vendor/foo.c -target aarch64-macos-gnu -o foo-mac.o
```


---

## 19 · Concurrency

Threads, locks and atomics — plus the 0.16 std.Io interface that replaced async/await.

### Threads
*ziglings 107–108*

`std.Thread.spawn` with any function and argument tuple; join or detach; shared state is your job.

**Spawn + join with a context struct**

```zig
const Job = struct { n: u32, out: u32 = 0 };

fn worker(job: *Job) void {
    job.out = job.n * 2;
}

pub fn main() !void {
    var job: Job = .{ .n = 21 };
    const t = try std.Thread.spawn(.{}, worker, .{&job});
    t.join(); // block until done (or: t.detach())
    std.debug.print("{d}\n", .{job.out}); // 42
}
```

| operation | notes |
| --- | --- |
| `std.Thread.spawn(.{}, f, .{args})` | returns `!Thread` — options struct first |
| `t.join()` | block until the thread finishes |
| `t.detach()` | let it run free; never join it |
| `std.Thread.current()` | handle for the calling thread |
| `std.Thread.getCpuCount()` | `!usize` — hardware parallelism |

> ⚠️ **Data races are UB.** Sharing mutable state across threads requires synchronization (locks) or atomics — the compiler will not stop you, so route anything shared through one of the next two entries.

### Mutex, Condition & Friends

The `std.Thread` synchronization primitives: Mutex, Condition, ResetEvent, Semaphore, RwLock.

| primitive | operations |
| --- | --- |
| `std.Thread.Mutex` | `lock` / `unlock` / `tryLock`; shared mode: `lockShared` / `unlockShared` |
| `std.Thread.Condition` | `wait(&mutex)` / `signal` / `broadcast` / `timedWait` |
| `std.Thread.ResetEvent` | `wait` / `set` / `reset` / `isSet` |
| `std.Thread.Semaphore` | `wait` / `post` |
| `std.Thread.RwLock` | `lock` / `unlock` / `lockShared` / `unlockShared` |

**Guard pattern: mutex next to its data**

```zig
const Counter = struct {
    mu: std.Thread.Mutex = .{},
    n: u32 = 0,

    fn bump(self: *Counter) void {
        self.mu.lock();
        defer self.mu.unlock(); // always unlock, even on early return
        self.n += 1; // protected critical section
    }
};
```

**ResetEvent: one-shot done signal**

```zig
var done: std.Thread.ResetEvent = .{};

fn work() void {
    defer done.set();
    // ... do the work ...
}

// another thread:
done.wait(); // blocks until set()
```

### Atomics

`std.atomic.Value(T)` wraps one value with explicit memory ordering; raw `@atomic*` builtins below it.

**Stop flag across threads**

```zig
var stop: std.atomic.Value(bool) = .init(false);

fn worker() void {
    while (!stop.load(.acquire)) {
        // ... work chunk ...
    }
}

// main thread:
stop.store(true, .release); // signal shutdown
```

| `std.atomic.Value(T)` op | call |
| --- | --- |
| create | `std.atomic.Value(T).init(v)` |
| read | `.load(.acquire)` |
| write | `.store(v, .release)` |
| exchange | `.swap(v, .seq_cst)` |
| read-modify-write | `.rmw(.Add, x, .seq_cst)` |

| raw builtin | purpose |
| --- | --- |
| `@atomicLoad(T, ptr, order)` | atomic read |
| `@atomicStore(T, ptr, v, order)` | atomic write |
| `@atomicRmw(T, ptr, op, v, order)` | atomic read-modify-write |
| `@cmpxchgStrong` / `@cmpxchgWeak` | compare-and-swap (weak may spuriously fail) |
| `@fence(order)` | standalone memory barrier |

- Memory orders: `.monotonic` (relaxed), `.acquire`, `.release`, `.acq_rel`, `.seq_cst`
- Default to `.seq_cst`; relax only with profiling evidence
- `.monotonic` for counters, `.acquire`/`.release` pairing for flag hand-offs

### std.Io: The Async Story (0.16)
*since 0.16 · ziglings 085–095*

async/await keywords are gone — concurrency is an interface (`std.Io`) with pluggable implementations.

> ⚠️ **History callout:** The `async` / `await` / `suspend` / `resume` keywords were **removed since 0.14**. Since **0.16**, I/O concurrency is expressed through the `std.Io` interface instead — an `io` handle flows through your program.

| abstraction | role |
| --- | --- |
| `std.Io.Future` | one task started from a function; await its result |
| `std.Io.Group` | many tasks; await or cancel all of them |
| `std.Io.Queue(T)` | MPMC channel between tasks |
| `std.Io.Batch` | batch many operations, submit together |
| `std.Io.Select` | wait on several operations at once |

| implementation | notes |
| --- | --- |
| `std.Io.Threaded` | default — operations block on a thread pool |
| `std.Io.Evented` | experimental green threads |
| `std.Io.Uring` / `Kqueue` / `Dispatch` | proof-of-concept backends |
| `std.Io.failing` | always fails — tests, no-IO builds |

**Illustrative shape (simplified)**

```zig
// SIMPLIFIED - the 0.16 std.Io API is evolving:
// consult the std docs for your exact version.
const fut = try io.async(work, .{ arg }); // start a task
const out = try fut.await(io); // join it

// many tasks: std.Io.Group
//   g.async(io, work, .{...}) for each, then g.await(io) / g.cancel(io)
```

- `-fsingle-threaded` — compile assuming one thread (enables optimizations, disables thread APIs)
- `-fno-single-threaded` — force the opposite
- Which backend runs is configured per root module — see the std.Io docs for your version

### Choosing a Tool

CPU parallelism → threads; I/O concurrency → std.Io; signaling → events; counters → atomics.

| you need | reach for |
| --- | --- |
| CPU parallelism | `std.Thread` (+ locks / atomics) |
| I/O concurrency | `std.Io` — `Future` / `Group` / `Queue(T)` |
| simple signaling | `ResetEvent` or `Condition` |
| counters / stop flags | `std.atomic.Value(T)` |
| per-task scratch memory | one `ArenaAllocator` per task |

Start with `std.Thread` for CPU-bound work. `std.Io.Threaded` gives I/O concurrency without changing your threading model; `std.Io.Evented` scales to many concurrent I/O operations on few threads.

> ⚠️ Ziglings exercises **085–095** teach the **removed** `async`/`await` keywords — treat them as historical. Ziglings exercises for the new `std.Io` interface are still pending.


---

## 20 · Gotchas & Tips

Where Zig bites: UB, undefined, version churn — and the philosophy behind the rules.

### Safety Checks vs UB

Debug and ReleaseSafe panic with a trace; ReleaseFast turns failed checks into UB; ReleaseSmall mostly drops them.

| check | Debug / ReleaseSafe | ReleaseFast | ReleaseSmall |
| --- | --- | --- | --- |
| array/slice bounds | panic + trace | UB | off |
| integer overflow | panic + trace | UB | wrapping (off) |
| `.?` on null | panic + trace | UB | off |
| `unreachable` reached | panic + trace | UB | off |
| division by zero | panic + trace | UB | off |
| cast truncation (`@intCast`) | panic + trace | UB | off |

Rule: **develop and test in Debug or ReleaseSafe**, ship ReleaseFast only with solid tests and coverage. ReleaseSmall trades most safety checks for binary size (overflow wraps silently).

- `unreachable` is a promise to the optimizer — keep it true in every mode
- `@intCast` and `.?` are checked in safe modes, unchecked elsewhere
- Out-of-range `@intFromFloat` is UB — range-check before converting

### undefined & Uninitialized

`undefined` means "no value" — reading it is UB in every build mode (0xAA fill makes it visible in Debug).

**Declare now, initialize before use**

```zig
var buf: [64]u8 = undefined; // declared, not initialized
@memset(&buf, 0); // MUST write before reading

const x: u32 = undefined;
_ = x + 1; // UB: reads undefined memory (0xAA fill in Debug)
```

> ⚠️ Reading `undefined` is **UB in every mode**. Debug fills with `0xAA` so the damage shows up as crashes or absurd values instead of silent luck.

- Defer-init pattern: `var v: T = undefined;` then assign **all** fields before first read
- Struct literals: initialize every field or give fields defaults — no partial init
- `@memset(ptr, 0)` bulk-initializes a buffer
- Legit uses: buffers a function will fully overwrite, or values the OS/writer fills before you read

### Pointer Pitfalls

Dangling after growth, misaligned casts, and the four pointer spellings people mix up.

**Invalidation after growth**

```zig
var list: std.ArrayList(u32) = .empty;
defer list.deinit(gpa);
try list.append(gpa, 1);
const first: *u32 = &list.items[0];
try list.append(gpa, 2); // may reallocate items
// 'first' may now DANGLE - retake &list.items[0]
```

- Alignment: casting to a stricter alignment via `@ptrCast` needs `@alignCast` — misaligned access is UB
- Use-after-free: reading after `free` (see **Lifetime Rules**)
- `[]T` (len known) vs `[*]T` (many, unknown len) vs `[*c]T` (C) vs `*T` (exactly one) — coerce deliberately
- Capture semantics: `for (items) |item|` gives a **copy**; `|*item|` gives `*T` so writes stick

**Copy vs by-reference capture**

```zig
for (items) |item| {} // item is a copy - writes are lost
for (items) |*item| { // item: *T - mutations persist
    item.* += 1;
}
```

### Language Surprises

Small rules that trip up newcomers: comptime_int locals, bool-only conditions, mandatory else, no shadowing.

- `!` negates a bool; `~` is bitwise-not for integers — there is no `~` for bools
- No `x++` / `x--`: write `x += 1`
- `if` conditions must be `bool` — no truthy integers or pointers
- `var x = 1;` is a **compile error** at runtime scope: the literal is `comptime_int`, so annotate the type
- Indexing takes `usize` — cast with `@intCast` (or `@as`) first
- Shadowing is forbidden: an inner `x` cannot hide an outer `x`
- Unused variables, parameters and errors are compile errors — discard with `_ = x;`
- Blocks are expressions: an `if` used as a value **requires** `else`
- `switch` must be exhaustive (add `else =>` to cover the rest)

**comptime_int strikes**

```zig
var x = 1; // ERROR: '1' is comptime_int, runtime vars need a type
var y: i32 = 1; // OK
```

**Value-context if requires else**

```zig
const maybe: ?u32 = null;
const v = if (maybe) |n| n else 0; // else is mandatory here
const s = switch (maybe) { .some => 1, .none => 0 }; // exhaustive too
```

### Version Migration Traps (0.15 → 0.16)
*since 0.16*

The rename-and-remove list that will break older code: containers, mem helpers, IO, @cImport, @Type.

| 0.15 and earlier | 0.16 |
| --- | --- |
| `var l = ArrayList(T).init(gpa)` | `var l: ArrayList(T) = .empty` + allocator per call |
| `std.heap.GeneralPurposeAllocator` | `std.heap.DebugAllocator` |
| `std.mem.trimLeft` / `trimRight` | `trimStart` / `trimEnd` |
| `std.mem.indexOf*` | `std.mem.find*` (`indexOf` → `find`, `lastIndexOf` → `findLast`) |
| `@cImport({ @cInclude(...) })` | `b.addTranslateC` + module import |
| `@Type(...)` | `@Int`, `@Float`, `@Array`, `@Pointer`, ... type builders |
| `std.process.argsAlloc()` | `init.minimal.args.toSlice(init.arena.allocator())` |
| `std.fs.cwd()` / `std.fs` | `std.Io.Dir` / `std.Io.File` with an `io` handle |
| `aw.writer()` method call | `aw.writer` **field** access |
| `std.Thread.Pool` | removed — use `std.Io` |
| managed `ArrayHashMap` | `std.array_hash_map.Auto` / `.String` / `.Custom` |
| global args / env | `main(init)` only — `init.minimal.args`, `init.environ_map` |

> ⚠️ `Writer.Allocating.writer` is a **field**, not a method — `aw.writer()` is a compile error; write `aw.writer.print(...)`. Full details: the 0.16.0 release notes.

### The Zen of Zig

Six lines that explain most design decisions — and most of the gotchas above.

- Communicate intent precisely.
- Edge cases matter.
- Only one way to do things.
- No hidden control flow.
- No hidden allocations.
- Together, we serve the users.

The design enforces it: allocation is explicit (the `Allocator` parameter), control flow is visible (errors are values, no exceptions, no operator overloading) — which is exactly why the gotchas in this section exist.

### Reading Compiler Errors

Zig errors explain coercions, offer `help:` hints, and can be produced without emitting a binary.

- Errors include **coercion explanations** ("expected type X, found Y") — read top to bottom
- `help:` lines usually contain the fix
- **Reference traces** show where each mismatched type came from
- Typecheck only: `zig build-exe main.zig -fno-emit-bin`
- Step-level detail for build failures: `zig build --summary all`
- `error: unused ...` → discard with `_ = x;` (or name it `_`)
- "expected type" mismatches → check the casting table (see **Quick Reference**)

**The discard fix**

```zig
fn f(gpa: std.mem.Allocator) !void {
    const s = try gpa.alloc(u8, 8); // error: unused local variable 's'
    _ = s; // fix: explicit discard
}
```

Zig's error messages are unusually pedagogical — when the compiler complains, the explanation is usually longer than the fix.


---

## 21 · Quick Reference

Print-and-pin tables: operators, literals, builtins, casts, and where to read more.

### Operators & Precedence

All operators, grouped highest to lowest precedence, with the wrap/saturate variants.

| precedence | operators | meaning |
| --- | --- | --- |
| 1 (highest) | `x.*  x.?  x[i]  x.f  f()  @b()` | postfix: deref, unwrap, index, field, call |
| 2 | `!  -  ~  &x` | unary: not, negate, bit-not, address-of |
| 3 | `*  /  %  **  *%  *|` | multiply, divide, remainder, power, wrap-mul, sat-mul |
| 4 | `+  -  ++  +|  +%  -|  -%` | add, subtract, **concat**, sat-add, wrap-add, sat-sub, wrap-sub |
| 5 | `<<  >>  <<|` | shift left/right, saturating shift left |
| 6 | `&  ^  |` | bitwise and, xor, or |
| 7 | `==  !=  <  >  <=  >=` | comparison |
| 8 | `and  or` | logical, short-circuit |
| 9 (lowest) | `=  +=  -=  *=  /=  %=  *=  **=  *%=  <<=  >>=  &=  |=  ^=  +|=  +%=  -|=  -%=  <<|=` | assignment (a statement, not an expression) |

- `++` concatenates arrays and slices (comptime-known result for arrays)
- `+%` / `-%` / `*%` wrap on overflow; `+|` / `-|` / `*|` saturate; `<<|` saturates the shift
- `and` / `or` short-circuit and require bool operands
- No operator overloading anywhere — operators always mean bit/math operations

### Literal Syntax

Every literal form: integer bases, chars, floats, strings, enum/struct/error literals, ranges.

| kind | examples |
| --- | --- |
| integers | `42`  `0x2A`  `0o52`  `0b101010`  `1_000_000` (separators) |
| characters | `'a'`  `'\n'`  `'\x1b'`  `'\u{263A}'` |
| floats | `3.14`  `1e9`  `0x1.8p3` (hex float) |
| strings | `"hi\n"`  `"\xAF"` (byte)  multiline with `\\` |
| enum literal | `.tag` |
| struct / tuple literal | `.{ .x = 1 }`  `.{ 1, 2 }` |
| errors | `error.OutOfMemory`  `error.Set{ .A, .B }` |
| switch ranges | `'a'...'z'`  `1...9` (three dots) |
| anon list → array | `.{ 1, 2, 3 }` coerces to `[3]u8` (anonymous list, since 0.15) |

**Literals in one glance**

```zig
const hex: u32 = 0xFF_00;
const nl: u8 = '\n';
const f: f64 = 0x1.8p3; // 12.0
const s: []const u8 = "hi\n" ++ "there";
const multi =
    \\line one
    \\line two
;
```

`.{ ... }` is the anonymous literal — it becomes a struct (`.{ .x = 1 }`), a tuple (`.{ 1, 2 }`), or an array/tuple depending on the target type.

### Builtin Functions Index

The `@`-functions grouped by purpose — introspection, casts, math, SIMD, compile-time, runtime.

| group | builtins |
| --- | --- |
| Introspection | `@typeInfo @typeName @TypeOf @sizeOf @alignOf @bitSizeOf @offsetOf @This @field @hasDecl @hasField @extern` |
| Casts | `@intCast @truncate @floatCast @floatFromInt @intFromFloat @enumFromInt @intFromEnum @bitCast @ptrCast @alignCast @constCast @volatileCast @addrSpaceCast` |
| Math | `@min @max @abs @sqrt @sin @cos @tan @exp @log @log2 @log10 @floor @ceil @round @trunc @mod @rem @divTrunc @divFloor @divExact @mulAdd @popCount @clz @ctz @byteSwap @bitReverse` |
| Overflow-aware | `@addWithOverflow @subWithOverflow @mulWithOverflow @shlWithOverflow` |
| SIMD | `@splat @reduce @select @shuffle @mulAdd` |
| Compile-time | `@compileError @compileLog @setEvalBranchQuota @setRuntimeSafety @embedFile @import @src @call @export` |
| Runtime | `@panic @breakpoint @fence @errorName @errorReturnTrace @returnAddress @frameAddress @prefetch` |
| Pointers | `@ptrFromInt @intFromPtr` |
| Type creators (0.16) | `@Int @Float @Array @Pointer @Struct @Union @Enum @Tuple @Opaque @Optional @ErrorUnion @ErrorSet @Fn @EnumLiteral` |

`@Type` was removed in **0.16** and replaced by the individual type creators in the last row. Full signatures live in the std docs; every builtin is documented in the language reference.

### Casting Cheat Table

Which builtin converts what — and where the safe-mode checks kick in.

| from → to | builtin | notes |
| --- | --- | --- |
| int → smaller int | `@intCast` | panics on truncation in safe modes |
| int → bigger int | coercion or `@intCast` | implicit up-cast just works |
| int → float | `@floatFromInt` | — |
| float → int | `@intFromFloat` | truncates; out-of-range is UB |
| float → smaller float | `@floatCast` | may lose precision |
| enum → int | `@intFromEnum` | — |
| int → enum | `@enumFromInt` | no validation — can create invalid tags |
| ptr → ptr | `@ptrCast` (+ `@alignCast`) | keep the alignment legal |
| bits reinterpret | `@bitCast` | source and target must have equal size |
| `[]T` → `[]const T` | coercion | implicit, free |
| `?T` → `T` | `.?` | panics on null in safe modes |
| `E!T` → `T` | `try` / `catch` | error handling, not a cast builtin |

**The big four**

```zig
const big: u64 = 300;
const small: i32 = @intCast(big); // panics if it doesn't fit (safe modes)
const approx: f64 = @floatFromInt(big);
const bits: u32 = @bitCast(@as(f32, 1.0)); // reinterpret same-size bits
```

When two types coerce implicitly, no builtin is needed — the "expected type X, found Y" error usually tells you which cast the compiler wants.

### Links & Resources

Official docs, std reference, ziglings, community — and where the 0.16 changes are listed.

| resource | what it is |
| --- | --- |
| ziglang.org | downloads, official docs, blog |
| ziglang.org/documentation/master/ | language reference |
| ziglang.org/documentation/master/std/ | standard library docs |
| zig.guide | community beginner guide |
| codeberg.org/ziglings/exercises | ziglings — fix small broken programs |
| ziggit.dev | community forum |
| github.com/ziglang/zig | source, issues, PRs |
| ziglang.org/download/0.16.0/release-notes.html | 0.16.0 release notes — everything that changed |
| cheats.rs | the Rust cheatsheet that inspired this site |

This site targets Zig **0.16.0**; `since` badges mark entries whose API arrived in 0.15 or 0.16. Ziglings exercise numbers appear throughout as cross-references — work through them top to bottom for muscle memory.

