TigerStyle: Zig Coding Guidelines
Distilled from TigerBeetle's production codebase. Safety > Performance > Developer Experience.
Capability Boundaries
✅ Strong Suits
- Writing high-performance Zig code (Safety > Performance > Developer Experience)
- Designing Zig data structures and APIs
- Reviewing Zig code style (assert usage, memory layout, naming conventions)
- Using comptime assert to verify design integrity
⚠️ Requirements
- User is writing or refactoring Zig code
- Needs high-performance / production-grade style guidance
❌ Out of Scope (with alternatives)
- Do not use this for standard library API lookup → use zig-0.16 skill instead
- Do not use this for code review workflow → use zig-code-review skill instead
- Do not use this for Zig beginner guide → use zig-0.16 skill instead
When to use
Use this skill when the user is writing, reviewing, or refactoring Zig code, asking about Zig idioms, assertions, memory layout, or API design.
Data Privacy
This skill does not collect, store, or transmit any user data. All content is derived from the publicly available TigerBeetle codebase style documentation.
TigerStyle: Zig Coding Guidelines
Distilled from TigerBeetle's TIGER_STYLE.md.
Quick Start
Example invocations:
Review this Zig code with TigerStyle
Design a TigerStyle-compliant Zig struct
What issues does this code have under TigerStyle?
Workflow
Step 1. Identify needs — Is the user reviewing, writing, or refactoring?
Step 2. Load guidelines — Refer to the Safety/Performance/Naming principles
Step 3. Check against rules — Verify against the Pre-Commit Checklist
Step 4. Output recommendations — Provide specific fixes per TigerStyle standards
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 or more arguments of the same type 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:
Audience
| User Type |
Usage |
| Performance-sensitive project developers |
Follow TigerStyle fully |
| Zig beginners |
Learn Zig best practices |
| Code reviewers |
Check code against TigerStyle standards |
Customization:
- Specify strictness level (full TigerStyle / partial rules)
- Specify focus dimension (safety / performance / naming)
Gotchas
- TigerStyle is a high bar — Designed for production performance-sensitive projects; not every project needs full adherence
- Don't overuse assert — Asserts detect programmer errors, not expected runtime errors
- Zero-dependency policy is not universal — TigerBeetle's policy is specific to its domain
- Comptime assert over runtime assert — Design integrity checks should happen at compile time
- Unit suffixes are mandatory — All fields with units must include a unit suffix (e.g.
_ms, _bytes)
FAQ
Q: What is the difference between TigerStyle and official Zig style?
A: TigerStyle is stricter, adding a hard 100-column limit, 4-space indentation, no recursion, and zero-dependency requirements.
Q: Must my project fully comply with TigerStyle?
A: No. TigerStyle is designed for high-frequency trading, databases, and other performance-sensitive domains. Ordinary projects can adopt what fits.
Q: How does TigerStyle ensure readability?
A: Through column width limits, naming conventions (significant-first, same-length alignment), and keeping functions small.
1---2name: zig-tiger-style3description: TigerStyle Zig coding guidelines — distilled from TigerBeetle's production codebase. Use whenever writing, reviewing, or refactoring Zig code, asking about Zig idioms, assertions, memory layout, naming conventions, code style, and API design.4---56# TigerStyle: Zig Coding Guidelines78> Distilled from TigerBeetle's production codebase. Safety > Performance > Developer Experience.910## Capability Boundaries1112### ✅ Strong Suits131. Writing high-performance Zig code (Safety > Performance > Developer Experience)142. Designing Zig data structures and APIs153. Reviewing Zig code style (assert usage, memory layout, naming conventions)164. Using comptime assert to verify design integrity1718### ⚠️ Requirements191. User is writing or refactoring Zig code202. Needs high-performance / production-grade style guidance2122### ❌ Out of Scope (with alternatives)231. Do not use this for standard library API lookup → use zig-0.16 skill instead242. Do not use this for code review workflow → use zig-code-review skill instead253. Do not use this for Zig beginner guide → use zig-0.16 skill instead2627## When to use2829Use this skill when the user is writing, reviewing, or refactoring Zig code, asking about Zig idioms, assertions, memory layout, or API design.3031## Data Privacy3233This skill does not collect, store, or transmit any user data. All content is derived from the publicly available TigerBeetle codebase style documentation.3435# TigerStyle: Zig Coding Guidelines3637Distilled from TigerBeetle's [TIGER_STYLE.md](https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TIGER_STYLE.md).38## Quick Start3940**Example invocations:**41```42Review this Zig code with TigerStyle43Design a TigerStyle-compliant Zig struct44What issues does this code have under TigerStyle?45```4647## Workflow4849Step 1. **Identify needs** — Is the user reviewing, writing, or refactoring?50Step 2. **Load guidelines** — Refer to the Safety/Performance/Naming principles51Step 3. **Check against rules** — Verify against the Pre-Commit Checklist52Step 4. **Output recommendations** — Provide specific fixes per TigerStyle standards5354## Design goal priority5556**Safety > Performance > Developer Experience**5758---5960## 1. Safety6162### Control Flow63- Use only **simple, explicit control flow**. No recursion unless provably bounded.64- Split compound conditions into nested `if/else` branches — ensure both the positive and negative65 spaces are handled or asserted.66- State invariants positively:6768 ```zig69 // preferred70 if (index < length) { ... } else { ... }7172 // avoid73 if (index >= length) { ... }74 ```7576- Every `if` branch should prompt the question: does a corresponding `else` also need to be handled?7778### Assertions79Assertions detect **programmer errors** — not expected runtime errors. The only correct response80to corrupt state is to crash. Assertions downgrade catastrophic correctness bugs into liveness bugs.8182- A function must not operate blindly on data it has not checked; assert arguments at the entry point.83- **Pair assertions**: for any property you want to enforce, add assertions on at least two different84 code paths (e.g. just before writing to disk, and immediately after reading back).85- Split compound assertions:8687 ```zig88 // preferred89 assert(a);90 assert(b);9192 // avoid93 assert(a and b);94 ```9596- Use a single-line `if` to assert an implication: `if (a) assert(b);`97- **Assert relationships between compile-time constants** to verify design integrity before the98 program even runs:99100 ```zig101 comptime assert(@sizeOf(Header) == 128);102 comptime assert(config.pipeline_max <= config.batch_max);103 ```104105- Assert both the **positive space** (what you expect to be true) and the **negative space** (what106 you expect to be false) — the boundary between valid and invalid is where bugs hide.107108### Memory109- Initialize large structs **in-place via an out pointer** to eliminate intermediate copies and110 guarantee pointer stability:111112 ```zig113 // preferred114 fn init(target: *LargeStruct) !void {115 target.* = .{ ... };116 }117118 // avoid119 fn init() !LargeStruct {120 return LargeStruct{ ... };121 }122 ```123124### Variable Scope125- Declare variables at the **smallest possible scope** to reduce the chance of misuse.126- Declare variables **close to where they are used** — do not introduce them before they are needed.127 This avoids POCPOU bugs (a distant cousin of TOCTOU).128129### Loops and Queues130- All loops and queues must have a **fixed upper bound** to prevent infinite loops or tail-latency131 spikes. Follow the fail-fast principle.132- Loops that genuinely cannot terminate (e.g. an event loop) must be explicitly asserted as such.133134### Error Handling135- **All errors must be handled.** Most catastrophic production failures stem from incorrect handling136 of non-fatal errors.137- Never discard error return values with `_`.138139### Other140- Use **explicitly-sized integer types** (`u32`, `i64`, etc.), avoid architecture-dependent `usize` when possible141- Enable and respect the **compiler's strictest warning settings** — zero tolerance for warnings.142- Do not react directly to external events inline; let the program run at its own pace (enables143 batching and maintains control-flow ownership).144- **Keep functions as small as possible.** When splitting, find semantically clean cut points:145 - Centralize all `if`/`switch` in the "parent" function; extract pure logic into helpers.146 - Let the parent own all mutable state; helpers compute what to change but don't apply it.147 - 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).148149---150151## 2. Performance152153- Solve performance in the **design phase** — the biggest wins (1000x) come from architecture,154 not post-hoc profiling.155- Do **back-of-the-envelope sketches** across the four resources (network, disk, memory, CPU) and156 their two characteristics (bandwidth, latency).157- Optimize slowest resources first: network → disk → memory → CPU, weighted by access frequency.158- **Batching** is the primary tool: amortize network, disk, memory, and CPU costs.159- Distinguish **control plane** from **data plane**; batching lets both coexist safely and fast.160- Extract hot-path loops into **standalone functions with primitive arguments** (no `self`) so the161 compiler can cache fields in registers and humans can spot redundant work:162163 ```zig164 // hot loop extracted, no self165 fn process_batch(items: []const Item, result: []Output) void { ... }166 ```167168- Be explicit. Do not rely on the compiler to do the right thing.169- **Always pass options explicitly** at library call sites — never rely on defaults:170171 ```zig172 // preferred173 @prefetch(a, .{ .cache = .data, .rw = .read, .locality = 3 });174175 // avoid176 @prefetch(a, .{});177 ```178179---180181## 3. Naming182183- In general, functions are `camelCase`, types are `PascalCase`, variables are `lowercase_with_underscores`.184 One exception to those rules is functions that return types. They are `PascalCase`:185 ```zig186 pub fn ArrayList(comptime T: type) type {187 return ArrayListAligned(T, null);188 }189 ```190- Normally, file names are `lowercase_with_underscore`. However, files that expose a type directly should be `PascalCase`.191- **Do not abbreviate variable names** (except primitive integer loop indices in sorts/matrices).192- Acronyms are fully capitalized: `VSRState`, not `VsrState`.193- **Append units and qualifiers to names**, ordered by descending significance, so the most194 important word comes first:195196 ```zig197 latency_ms_max // not max_latency_ms198 latency_ms_min // aligns nicely with the above199 message_size_max200 ```201202- Choose related names with the **same character count** so they align visually:203204 ```zig205 source // same length as target206 target207 source_offset208 target_offset209 ```210211- Name helper/callback functions with the caller's name as a prefix:212 `read_sector()` → `read_sector_callback()`213- **Callbacks go last** in the parameter list (mirrors invocation order).214- Infuse names with meaning: `gpa: Allocator` and `arena: Allocator` are far more informative215 than `allocator: Allocator`.216- Functions that take two or more arguments of the same type must use a named `options: struct` parameter to prevent argument confusion.217218### Struct and File Layout219220```zig221// Struct order: fields → type definitions → methods222time: Time,223process_id: ProcessID,224225const ProcessID = struct { cluster: u128, replica: u8 };226const Tracer = @This();227228pub fn init(gpa: std.mem.Allocator, time: Time) !Tracer { ... }229```230231- The `main` function goes at the top of the file — readers see the most important thing first.232- Promote complex nested types to top-level structs.233234---235236## 4. Comments237238- Comments are full sentences: space after `//`, capital letter, ending with a period (or colon239 when introducing something). Inline end-of-line comments may be phrases without punctuation.240- **Always say why.** Code shows what and how; comments explain the reasoning behind decisions.241- Add a description at the top of tests explaining the goal and methodology.242- On occasion, use an obviously-true assertion *instead of* a comment to document a critical,243 surprising invariant — the assertion is stronger documentation.244245---246247## 5. Formatting248249- Always run `zig fmt`.250- Use **4 spaces** of indentation (more visually obvious than 2 at a distance).251- **Hard limit of 100 columns per line**, no exceptions. Add a trailing comma and let `zig fmt`252 handle the wrapping.253- **Always add braces to `if` statements** unless the whole thing fits on one line:254255 ```zig256 // single-line ok without braces257 if (ok) return;258259 // multi-line always needs braces260 if (condition) {261 do_something();262 }263 ```264265### Division — be explicit about rounding intent266267```zig268@divExact(a, b) // asserts no remainder269@divFloor(a, b) // rounds toward negative infinity270div_ceil(a, b) // rounds toward positive infinity271```272273---274275## 6. Off-by-One Errors276277`index` (0-based), `count` (1-based), and `size` (= count × unit) are **distinct types** with278clear conversion rules:279280- `index` → `count`: add 1281- `count` → `size`: multiply by the unit size282- Include units and qualifiers in variable names (see Naming) to make these conversions visible.283284---285286## 7. Dependencies and Tooling287288- **Zero-dependencies policy**: no external dependencies beyond the Zig toolchain.289- Write scripts as `scripts/*.zig` instead of `*.sh` — cross-platform, type-safe, more reliable.290- Standardize on Zig for tooling to reduce dimensionality as the team grows.291292---293294## Pre-Commit Checklist295296Before submitting, verify:297298- [ ] All lines are <= 100 columns; `zig fmt` has been run299- [ ] All errors are handled (no `_` discards)300- [ ] Variable names include units/qualifiers and are not abbreviated301- [ ] Compound conditions are split into nested `if/else`302- [ ] All loops have an explicit upper bound303- [ ] Comments explain *why*, not just *what*304- [ ] Compile-time constant relationships are verified with `comptime assert`305306## Audience307308| User Type | Usage |309|-----------|-------|310| **Performance-sensitive project developers** | Follow TigerStyle fully |311| **Zig beginners** | Learn Zig best practices |312| **Code reviewers** | Check code against TigerStyle standards |313314Customization:315- Specify strictness level (full TigerStyle / partial rules)316- Specify focus dimension (safety / performance / naming)317318## Gotchas3193201. **TigerStyle is a high bar** — Designed for production performance-sensitive projects; not every project needs full adherence3212. **Don't overuse assert** — Asserts detect programmer errors, not expected runtime errors3223. **Zero-dependency policy is not universal** — TigerBeetle's policy is specific to its domain3234. **Comptime assert over runtime assert** — Design integrity checks should happen at compile time3245. **Unit suffixes are mandatory** — All fields with units must include a unit suffix (e.g. `_ms`, `_bytes`)325326## FAQ327328**Q: What is the difference between TigerStyle and official Zig style?**329A: TigerStyle is stricter, adding a hard 100-column limit, 4-space indentation, no recursion, and zero-dependency requirements.330331**Q: Must my project fully comply with TigerStyle?**332A: No. TigerStyle is designed for high-frequency trading, databases, and other performance-sensitive domains. Ordinary projects can adopt what fits.333334**Q: How does TigerStyle ensure readability?**335A: Through column width limits, naming conventions (significant-first, same-length alignment), and keeping functions small.336