zig-tiger-style について
TigerBeetle の TIGER_STYLE.md を基にまとめた Zig コーディングガイドラインスキルです。安全性と性能を重視する Zig 開発者向けに、構成、命名、書式、レビューの指針を示します。
主な機能
- 01明示的な制御フローとアサーション
- 02上限付きループと完全なエラー処理
- 03メモリと性能設計の指針
- 04命名、コメント、ファイル構成の規則
- 05フォーマットとコミット前チェックリスト
活用シーン
- 01Zig コードの安全性の確認
- 02Zig プロジェクトのチーム規約策定
- 03コミット前のコード確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ zigcc/ skills --skill zig-tiger-styleインストール後、Claude Code で/zig-tiger-styleを入力して読み込まれたか確認します。
詳細
TigerStyle:Zig コーディングガイドライン
TigerBeetle の TIGER_STYLE.md を要約したものです。 設計目標の優先順位:安全性 > パフォーマンス > 開発者エクスペリエンス。
1. 安全性
制御フロー
-
単純で明示的な制御フローのみを使用します。上限が証明できる場合を除き、再帰は使いません。
-
複合条件は入れ子の
if/else分岐に分割し、肯定側と否定側の両方を処理するか、アサートします。 -
不変条件は肯定形で記述します。
// preferred if (index < length) { ... } else { ... } // avoid if (index >= length) { ... } -
ifの各分岐で、対応するelseも処理する必要があるかを検討します。
アサーション
アサーションはプログラマーのミスを検出するためのもので、予期される実行時エラーを検出するものではありません。破損した状態に対する唯一の正しい対応は、クラッシュさせることです。アサーションによって、壊滅的な正確性のバグを活性のバグへと格下げできます。
-
確認していないデータを関数が盲目的に処理してはいけません。関数の入口で引数をアサートします。
-
ペアアサーション:適用したいプロパティごとに、異なるコードパスを少なくとも2つ選び、そこでアサーションを追加します(例:ディスクへの書き込み直前と、読み戻し直後)。
-
複合アサーションは分割します。
// preferred assert(a); assert(b); // avoid assert(a and b); -
含意をアサートするには、単一行の
ifを使います:if (a) assert(b); -
コンパイル時定数間の関係をアサートし、プログラムの実行前に設計の整合性を検証します。
comptime assert(@sizeOf(Header) == 128); comptime assert(config.pipeline_max <= config.batch_max); -
肯定側(真であると期待すること)と否定側(偽であると期待すること)の両方をアサートします。有効と無効の境界にはバグが潜んでいます。
メモリ
-
大きな構造体はアウトポインター経由でその場で初期化し、中間コピーをなくしてポインターの安定性を保証します。
// preferred fn init(target: *LargeStruct) !void { target.* = .{ ... }; } // avoid fn init() !LargeStruct { return LargeStruct{ ... }; }
変数のスコープ
- 誤用の可能性を減らすため、変数は可能な限り狭いスコープで宣言します。
- 変数は使用箇所の近くで宣言し、必要になる前に導入しないでください。これにより、TOCTOU の遠縁にあたる POCPOU バグを防げます。
ループとキュー
- 無限ループやテールレイテンシーの急増を防ぐため、すべてのループとキューには固定の上限を設けます。フェイルファストの原則に従います。
- イベントループなど、実際に終了しないループは、そのように明示的にアサートします。
エラー処理
- すべてのエラーを処理する必要があります。 本番環境で発生する壊滅的な障害の多くは、致命的ではないエラーの処理を誤ることが原因です。
_を使ってエラーの戻り値を決して破棄しないでください。
その他
- サイズが明示された整数型(
u32、i64など)を使用し、可能な限りアーキテクチャーに依存するusizeを避けます。 - コンパイラーの最も厳格な警告設定を有効にして遵守し、警告は一切許容しません。
- 外部イベントにインラインで直接反応しないでください。プログラム自身のペースで処理することで、バッチ処理が可能になり、制御フローの所有権を維持できます。
- 関数はできるだけ小さく保ちます。 分割する際は、意味的に適切な分割点を選びます。
- すべての
if/switchを「親」関数に集約し、純粋なロジックをヘルパーに切り出します。 - すべての可変状態は親に持たせます。ヘルパーは何を変更するかを計算しますが、変更を適用しません。
- 目安:「
ifを上へ、forを下へ」。
- すべての
2. パフォーマンス
-
パフォーマンスの問題は設計段階で解決します。最も大きな改善(1000倍)は、事後のプロファイリングではなく、アーキテクチャから生まれます。
-
4つのリソース(ネットワーク、ディスク、メモリ、CPU)と、それぞれの2つの特性(帯域幅、レイテンシ)について、概算のスケッチを作成します。
-
アクセス頻度を考慮し、遅いリソースから最適化します。ネットワーク → ディスク → メモリ → CPU の順です。
-
主な手段はバッチ処理です。ネットワーク、ディスク、メモリ、CPU のコストを償却できます。
-
コントロールプレーンとデータプレーンを区別します。バッチ処理により、両者を安全かつ高速に共存させられます。
-
ホットパスのループを、プリミティブ型の引数を取るスタンドアロン関数(
selfなし)に切り出します。これにより、コンパイラーはフィールドをレジスターにキャッシュでき、人間も重複した処理を見つけやすくなります。// hot loop extracted, no self fn process_batch(items: []const Item, result: []Output) void { ... } -
明示的に記述します。コンパイラーが適切に処理してくれることを前提にしないでください。
-
ライブラリー呼び出しでは、常にオプションを明示的に渡します。デフォルト値に依存しないでください。
// preferred @prefetch(a, .{ .cache = .data, .rw = .read, .locality = 3 }); // avoid @prefetch(a, .{});
3. 命名
-
一般に、関数は
camelCase、型はPascalCase、変数はlowercase_with_underscoresにします。ただし、型を返す関数は例外で、PascalCaseにします。pub fn ArrayList(comptime T: type) type { return ArrayListAligned(T, null); } -
通常、ファイル名は
lowercase_with_underscoreにします。ただし、型を直接公開するファイルはPascalCaseにします。 -
変数名を省略しないでください(ソートや行列で使うプリミティブ型の整数ループインデックスを除きます)。
-
頭字語はすべて大文字にします。
VsrStateではなくVSRStateとします。 -
単位や修飾語を名前に付けます。重要度の高い順に並べ、最も重要な単語を先頭にします。
latency_ms_max // not max_latency_ms latency_ms_min // aligns nicely with the above message_size_max -
関連する名前は、同じ文字数にして視覚的に揃えます。
source // same length as target target source_offset target_offset -
ヘルパー関数やコールバック関数の名前には、呼び出し元の名前を接頭辞として付けます。
read_sector()→read_sector_callback() -
コールバックはパラメーターリストの最後に置きます(呼び出し順と一致させます)。
-
名前に意味を持たせます。
gpa: Allocatorやarena: Allocatorは、allocator: Allocatorよりもはるかに情報量があります。 -
同じ型の引数を2つ以上取る関数では、引数の取り違えを防ぐため、名前付きの
options: structパラメーターを使用します。
構造体とファイルのレイアウト
// 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 { ... }
main関数はファイルの先頭に置きます。読み手が最も重要な内容を最初に確認できます。- 複雑なネスト型は、トップレベルの構造体に昇格させます。
4. コメント
- コメントは完全な文にします。
//の後にスペースを入れ、大文字で始め、ピリオドで終えます(何かを導入する場合はコロンで終えます)。行末のインラインコメントは、句読点のない語句でもかまいません。 - **理由を必ず説明します。**コードは何をどのように行うかを示し、コメントは判断の根拠を説明します。
- テストの先頭に、目的と手法を説明する記述を追加します。
- 場合によっては、コメントの代わりに、明らかに真であるアサーションを使って重要かつ意外な不変条件を文書化します。アサーションのほうが、より強い説明になります。
5. 書式設定
-
必ず
zig fmtを実行します。 -
インデントには4つのスペースを使います(離れた位置から見たとき、2つよりも視認性が高いためです)。
-
1行あたり100カラムを上限とし、例外は認めません。末尾にカンマを付けて、折り返しは
zig fmtに任せます。 -
行全体が1行に収まる場合を除き、
if文には必ず波括弧を付けます。// single-line ok without braces if (ok) return; // multi-line always needs braces if (condition) { do_something(); }
除算 — 丸めの意図を明示する
@divExact(a, b) // asserts no remainder
@divFloor(a, b) // rounds toward negative infinity
div_ceil(a, b) // rounds toward positive infinity
6. 1つずれるエラー
index(0始まり)、count(1始まり)、size(=count × 単位サイズ)は、変換規則が明確に定められた別個の型です。
index→count:1を加算count→size:単位サイズを乗算- 変数名に単位や修飾語を含め(「命名」を参照)、これらの変換が見てわかるようにします。
7. 依存関係とツール
- 依存関係ゼロの方針:Zigツールチェーン以外の外部依存関係を持ちません。
- スクリプトは
*.shではなくscripts/*.zigとして記述します。クロスプラットフォームで、型安全性が高く、信頼性も向上します。 - チームの成長に伴う複雑さを抑えるため、ツールにはZigを標準採用します。
コミット前のチェックリスト
提出する前に、以下を確認してください。
- すべての行が 100 列以内であり、
zig fmtを実行済みである - すべてのエラーが処理されている(
_による破棄がない) - 変数名に単位や修飾情報が含まれ、省略形が使われていない
- 複合条件が入れ子の
if/elseに分割されている - すべてのループに明示的な上限が設定されている
- コメントで単に「何を」ではなく「なぜ」を説明している
- コンパイル時定数間の関係が
comptime assertで検証されている