TigerStyle: Zig Coding Guidelines
Distilled from TigerBeetle's TIGER_STYLE.md.
Design goal priority: Safety > Performance > Developer Experience.
1. Safety
Control Flow
Use only simple, explicit control flow. No recursion unless provably bounded.
Split compound conditions into nested if/else branches — ensure both the positive and negative
spaces are handled or asserted.
State invariants positively:
// preferred
if (index < length) { ... } else { ... }
// avoid
if (index >= length) { ... }
Every if branch should prompt the question: does a corresponding else also need to be handled?
Assertions
Assertions detect programmer errors — not expected runtime errors. The only correct response
to corrupt state is to crash. Assertions downgrade catastrophic correctness bugs into liveness bugs.
A function must not operate blindly on data it has not checked; assert arguments at the entry point.
Pair assertions: for any property you want to enforce, add assertions on at least two different
code paths (e.g. just before writing to disk, and immediately after reading back).
Split compound assertions:
// preferred
assert(a);
assert(b);
// avoid
assert(a and b);
Use a single-line if to assert an implication: if (a) assert(b);
Assert relationships between compile-time constants to verify design integrity before the
program even runs:
comptime assert(@sizeOf(Header) == 128);
comptime assert(config.pipeline_max <= config.batch_max);
Assert both the positive space (what you expect to be true) and the negative space (what
you expect to be false) — the boundary between valid and invalid is where bugs hide.
Memory
Initialize large structs in-place via an out pointer to eliminate intermediate copies and
guarantee pointer stability:
// preferred
fn init(target: *LargeStruct) !void {
target.* = .{ ... };
}
// avoid
fn init() !LargeStruct {
return LargeStruct{ ... };
}
Variable Scope
- Declare variables at the smallest possible scope to reduce the chance of misuse.
- Declare variables close to where they are used — do not introduce them before they are needed.
This avoids POCPOU bugs (a distant cousin of TOCTOU).
Loops and Queues
- All loops and queues must have a fixed upper bound to prevent infinite loops or tail-latency
spikes. Follow the fail-fast principle.
- Loops that genuinely cannot terminate (e.g. an event loop) must be explicitly asserted as such.
Error Handling
- All errors must be handled. Most catastrophic production failures stem from incorrect handling
of non-fatal errors.
- Never discard error return values with
_.
Other
- Use explicitly-sized integer types (
u32, i64, etc.), avoid architecture-dependent usize when possible
- Enable and respect the compiler's strictest warning settings — zero tolerance for warnings.
- Do not react directly to external events inline; let the program run at its own pace (enables
batching and maintains control-flow ownership).
- Keep functions as small as possible. When splitting, find semantically clean cut points:
- Centralize all
if/switch in the "parent" function; extract pure logic into helpers.
- Let the parent own all mutable state; helpers compute what to change but don't apply it.
- Rule of thumb: "push
ifs up and fors down".
2. Performance
Solve performance in the design phase — the biggest wins (1000x) come from architecture,
not post-hoc profiling.
Do back-of-the-envelope sketches across the four resources (network, disk, memory, CPU) and
their two characteristics (bandwidth, latency).
Optimize slowest resources first: network → disk → memory → CPU, weighted by access frequency.
Batching is the primary tool: amortize network, disk, memory, and CPU costs.
Distinguish control plane from data plane; batching lets both coexist safely and fast.
Extract hot-path loops into standalone functions with primitive arguments (no self) so the
compiler can cache fields in registers and humans can spot redundant work:
// hot loop extracted, no self
fn process_batch(items: []const Item, result: []Output) void { ... }
Be explicit. Do not rely on the compiler to do the right thing.
Always pass options explicitly at library call sites — never rely on defaults:
// preferred
@prefetch(a, .{ .cache = .data, .rw = .read, .locality = 3 });
// avoid
@prefetch(a, .{});
3. Naming
In general, functions are camelCase, types are PascalCase, variables are lowercase_with_underscores.
One exception to those rules is functions that return types. They are PascalCase:
pub fn ArrayList(comptime T: type) type {
return ArrayListAligned(T, null);
}
Normally, file names are lowercase_with_underscore. However, files that expose a type directly should be PascalCase.
Do not abbreviate variable names (except primitive integer loop indices in sorts/matrices).
Acronyms are fully capitalized: VSRState, not VsrState.
Append units and qualifiers to names, ordered by descending significance, so the most
important word comes first:
latency_ms_max // not max_latency_ms
latency_ms_min // aligns nicely with the above
message_size_max
Choose related names with the same character count so they align visually:
source // same length as target
target
source_offset
target_offset
Name helper/callback functions with the caller's name as a prefix:
read_sector() → read_sector_callback()
Callbacks go last in the parameter list (mirrors invocation order).
Infuse names with meaning: gpa: Allocator and arena: Allocator are far more informative
than allocator: Allocator.
Functions that take two u64 arguments must use a named options: struct parameter to prevent argument confusion.
Struct and File Layout
// Struct order: fields → type definitions → methods
time: Time,
process_id: ProcessID,
const ProcessID = struct { cluster: u128, replica: u8 };
const Tracer = @This();
pub fn init(gpa: std.mem.Allocator, time: Time) !Tracer { ... }
- The
main function goes at the top of the file — readers see the most important thing first.
- Promote complex nested types to top-level structs.
4. Comments
- Comments are full sentences: space after
//, capital letter, ending with a period (or colon
when introducing something). Inline end-of-line comments may be phrases without punctuation.
- Always say why. Code shows what and how; comments explain the reasoning behind decisions.
- Add a description at the top of tests explaining the goal and methodology.
- On occasion, use an obviously-true assertion instead of a comment to document a critical,
surprising invariant — the assertion is stronger documentation.
5. Formatting
Always run zig fmt.
Use 4 spaces of indentation (more visually obvious than 2 at a distance).
Hard limit of 100 columns per line, no exceptions. Add a trailing comma and let zig fmt
handle the wrapping.
Always add braces to if statements unless the whole thing fits on one line:
// single-line ok without braces
if (ok) return;
// multi-line always needs braces
if (condition) {
do_something();
}
Division — be explicit about rounding intent
@divExact(a, b) // asserts no remainder
@divFloor(a, b) // rounds toward negative infinity
div_ceil(a, b) // rounds toward positive infinity
6. Off-by-One Errors
index (0-based), count (1-based), and size (= count × unit) are distinct types with
clear conversion rules:
index → count: add 1
count → size: multiply by the unit size
- Include units and qualifiers in variable names (see Naming) to make these conversions visible.
7. Dependencies and Tooling
- Zero-dependencies policy: no external dependencies beyond the Zig toolchain.
- Write scripts as
scripts/*.zig instead of *.sh — cross-platform, type-safe, more reliable.
- Standardize on Zig for tooling to reduce dimensionality as the team grows.
Pre-Commit Checklist
Before submitting, verify:
Source: jiacai2050/zigcli — distributed by TomeVault.
1---2name: jiacai2050-zigcli-zigcli3description: TigerStyle: Zig Coding Guidelines4---56# TigerStyle: Zig Coding Guidelines78Distilled from TigerBeetle's [TIGER_STYLE.md](https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TIGER_STYLE.md).9Design goal priority: **Safety > Performance > Developer Experience**.1011---1213## 1. Safety1415### Control Flow16- Use only **simple, explicit control flow**. No recursion unless provably bounded.17- Split compound conditions into nested `if/else` branches — ensure both the positive and negative18 spaces are handled or asserted.19- State invariants positively:2021 ```zig22 // preferred23 if (index < length) { ... } else { ... }2425 // avoid26 if (index >= length) { ... }27 ```2829- Every `if` branch should prompt the question: does a corresponding `else` also need to be handled?3031### Assertions32Assertions detect **programmer errors** — not expected runtime errors. The only correct response33to corrupt state is to crash. Assertions downgrade catastrophic correctness bugs into liveness bugs.3435- A function must not operate blindly on data it has not checked; assert arguments at the entry point.36- **Pair assertions**: for any property you want to enforce, add assertions on at least two different37 code paths (e.g. just before writing to disk, and immediately after reading back).38- Split compound assertions:3940 ```zig41 // preferred42 assert(a);43 assert(b);4445 // avoid46 assert(a and b);47 ```4849- Use a single-line `if` to assert an implication: `if (a) assert(b);`50- **Assert relationships between compile-time constants** to verify design integrity before the51 program even runs:5253 ```zig54 comptime assert(@sizeOf(Header) == 128);55 comptime assert(config.pipeline_max <= config.batch_max);56 ```5758- Assert both the **positive space** (what you expect to be true) and the **negative space** (what59 you expect to be false) — the boundary between valid and invalid is where bugs hide.6061### Memory62- Initialize large structs **in-place via an out pointer** to eliminate intermediate copies and63 guarantee pointer stability:6465 ```zig66 // preferred67 fn init(target: *LargeStruct) !void {68 target.* = .{ ... };69 }7071 // avoid72 fn init() !LargeStruct {73 return LargeStruct{ ... };74 }75 ```7677### Variable Scope78- Declare variables at the **smallest possible scope** to reduce the chance of misuse.79- Declare variables **close to where they are used** — do not introduce them before they are needed.80 This avoids POCPOU bugs (a distant cousin of TOCTOU).8182### Loops and Queues83- All loops and queues must have a **fixed upper bound** to prevent infinite loops or tail-latency84 spikes. Follow the fail-fast principle.85- Loops that genuinely cannot terminate (e.g. an event loop) must be explicitly asserted as such.8687### Error Handling88- **All errors must be handled.** Most catastrophic production failures stem from incorrect handling89 of non-fatal errors.90- Never discard error return values with `_`.9192### Other93- Use **explicitly-sized integer types** (`u32`, `i64`, etc.), avoid architecture-dependent `usize` when possible94- Enable and respect the **compiler's strictest warning settings** — zero tolerance for warnings.95- Do not react directly to external events inline; let the program run at its own pace (enables96 batching and maintains control-flow ownership).97- **Keep functions as small as possible.** When splitting, find semantically clean cut points:98 - Centralize all `if`/`switch` in the "parent" function; extract pure logic into helpers.99 - Let the parent own all mutable state; helpers compute what to change but don't apply it.100 - Rule of thumb: ["push `if`s up and `for`s down"](https://matklad.github.io/2023/11/15/push-ifs-up-and-fors-down.html).101102---103104## 2. Performance105106- Solve performance in the **design phase** — the biggest wins (1000x) come from architecture,107 not post-hoc profiling.108- Do **back-of-the-envelope sketches** across the four resources (network, disk, memory, CPU) and109 their two characteristics (bandwidth, latency).110- Optimize slowest resources first: network → disk → memory → CPU, weighted by access frequency.111- **Batching** is the primary tool: amortize network, disk, memory, and CPU costs.112- Distinguish **control plane** from **data plane**; batching lets both coexist safely and fast.113- Extract hot-path loops into **standalone functions with primitive arguments** (no `self`) so the114 compiler can cache fields in registers and humans can spot redundant work:115116 ```zig117 // hot loop extracted, no self118 fn process_batch(items: []const Item, result: []Output) void { ... }119 ```120121- Be explicit. Do not rely on the compiler to do the right thing.122- **Always pass options explicitly** at library call sites — never rely on defaults:123124 ```zig125 // preferred126 @prefetch(a, .{ .cache = .data, .rw = .read, .locality = 3 });127128 // avoid129 @prefetch(a, .{});130 ```131132---133134## 3. Naming135136- In general, functions are `camelCase`, types are `PascalCase`, variables are `lowercase_with_underscores`.137 One exception to those rules is functions that return types. They are `PascalCase`:138 ```zig139 pub fn ArrayList(comptime T: type) type {140 return ArrayListAligned(T, null);141 }142 ```143- Normally, file names are `lowercase_with_underscore`. However, files that expose a type directly should be `PascalCase`.144- **Do not abbreviate variable names** (except primitive integer loop indices in sorts/matrices).145- Acronyms are fully capitalized: `VSRState`, not `VsrState`.146- **Append units and qualifiers to names**, ordered by descending significance, so the most147 important word comes first:148149 ```zig150 latency_ms_max // not max_latency_ms151 latency_ms_min // aligns nicely with the above152 message_size_max153 ```154155- Choose related names with the **same character count** so they align visually:156157 ```zig158 source // same length as target159 target160 source_offset161 target_offset162 ```163164- Name helper/callback functions with the caller's name as a prefix:165 `read_sector()` → `read_sector_callback()`166- **Callbacks go last** in the parameter list (mirrors invocation order).167- Infuse names with meaning: `gpa: Allocator` and `arena: Allocator` are far more informative168 than `allocator: Allocator`.169- Functions that take two `u64` arguments must use a named `options: struct` parameter to prevent argument confusion.170171### Struct and File Layout172173```zig174// Struct order: fields → type definitions → methods175time: Time,176process_id: ProcessID,177178const ProcessID = struct { cluster: u128, replica: u8 };179const Tracer = @This();180181pub fn init(gpa: std.mem.Allocator, time: Time) !Tracer { ... }182```183184- The `main` function goes at the top of the file — readers see the most important thing first.185- Promote complex nested types to top-level structs.186187---188189## 4. Comments190191- Comments are full sentences: space after `//`, capital letter, ending with a period (or colon192 when introducing something). Inline end-of-line comments may be phrases without punctuation.193- **Always say why.** Code shows what and how; comments explain the reasoning behind decisions.194- Add a description at the top of tests explaining the goal and methodology.195- On occasion, use an obviously-true assertion *instead of* a comment to document a critical,196 surprising invariant — the assertion is stronger documentation.197198---199200## 5. Formatting201202- Always run `zig fmt`.203- Use **4 spaces** of indentation (more visually obvious than 2 at a distance).204- **Hard limit of 100 columns per line**, no exceptions. Add a trailing comma and let `zig fmt`205 handle the wrapping.206- **Always add braces to `if` statements** unless the whole thing fits on one line:207208 ```zig209 // single-line ok without braces210 if (ok) return;211212 // multi-line always needs braces213 if (condition) {214 do_something();215 }216 ```217218### Division — be explicit about rounding intent219220```zig221@divExact(a, b) // asserts no remainder222@divFloor(a, b) // rounds toward negative infinity223div_ceil(a, b) // rounds toward positive infinity224```225226---227228## 6. Off-by-One Errors229230`index` (0-based), `count` (1-based), and `size` (= count × unit) are **distinct types** with231clear conversion rules:232233- `index` → `count`: add 1234- `count` → `size`: multiply by the unit size235- Include units and qualifiers in variable names (see Naming) to make these conversions visible.236237---238239## 7. Dependencies and Tooling240241- **Zero-dependencies policy**: no external dependencies beyond the Zig toolchain.242- Write scripts as `scripts/*.zig` instead of `*.sh` — cross-platform, type-safe, more reliable.243- Standardize on Zig for tooling to reduce dimensionality as the team grows.244245---246247## Pre-Commit Checklist248249Before submitting, verify:250251- [ ] All lines are <= 100 columns; `zig fmt` has been run252- [ ] All errors are handled (no `_` discards)253- [ ] Variable names include units/qualifiers and are not abbreviated254- [ ] Compound conditions are split into nested `if/else`255- [ ] All loops have an explicit upper bound256- [ ] Comments explain *why*, not just *what*257- [ ] Compile-time constant relationships are verified with `comptime assert`258259---260> Source: [jiacai2050/zigcli](https://github.com/jiacai2050/zigcli) — distributed by [TomeVault](https://tomevault.io).261<!-- tomevault:4.0:skill_md:2026-06-29 -->