【入門編】HHVM型チェッカーが検知する『デッドコード』の静的解析アルゴリズム:到達不能コードを排除する – Hack言語 コア・静的型システムとHHVMのアーキテクチャ解析バイブル

こんにちは!HackとHHVMのディープな世界へようこそ。

PHPから移行してきた方や、TypeScriptやRustなどの静的型付き言語からHackに触れ始めた方は、Hackの型チェッカー(`hh_client`)の「圧倒的な速さと厳密さ」に驚かされているのではないでしょうか。

HackのStrict Mode(厳格モード)では、単に「変数の型が合っているか」を調べるだけでなく、「そのコード、絶対に実行されませんよね?」という到達不能コード(デッドコード)をミリ秒単位で冷徹に検出し、容赦なくエラーとして報告してくれます。

「動かないコードが残っていても別に実害はないのでは?」と思うかもしれません。しかし、デッドコードの放置はバグの温床であり、ロジックの矛盾そのものです。

今回は、HHVMの型チェッカーがどのようなアルゴリズムで実行パスを解析し、デッドコードをあぶり出しているのかについて、基礎から分かりやすく紐解いていきましょう!ここをクリアすれば、Hackのコードフロー解析の基本はバッチリマスターできますよ。

—

1. そもそも「デッドコード(到達不能コード)」とは?

プログラミングにおけるデッドコードとは、「プログラムのどの実行経路(パス)を通っても、絶対に処理が到達しないコード」のことです。

例えば、以下のようなコードを見てみましょう。

<>

function calculate_discount(int $price, bool $is_vip): int {
if ($is_vip) {
return (int)($price 0.8);
} else {
return $price;
}

// ここに書いたコードは絶対に実行されない!
$log_message = “Calculation finished”; // ← 型チェッカーがエラーを出す
echo $log_message;
}

人間が見ても「`if` でも `else` でも `return` しているから、その下には行かないな」と分かりますよね。
Hackの型チェッカーは、これをコード全体にわたって極めて厳密に計算しています。

—

2. 型チェッカーはどうやって見抜く?「制御フローグラフ(CFG)」の仕組み

型チェッカーが裏側で行っているのは、コードを上から下へ読むだけの単純な作業ではありません。プログラムを「制御フローグラフ(Control Flow Graph: CFG)」という地図のような構造に変換して解析しています。

イメージを図にしてみましょう。

[ 開始: calculate_discount ]
│
〈 $is_vip は真? 〉
╱ ╲
( YES ) ( NO )
│ │
[ 20%引きを return ] [ 定価を return ]
│ │
▼ ▼
【 関数終了 】 【 関数終了 】

── どこからも線が繋がらない ──
│
▼
[ $log_message = … ] ← ★ 到達不能!(デッドコード検知!)

CFG解析の3ステップ

1. 基本ブロックの分割: コードを「途中に分岐がない一本道の命令の塊」に分けます。
2. エッジ(矢印)の接続: `if`, `else`, `return`, `throw`, `loop` などの制御文に従って、ブロック同士を矢印で繋ぎます。
3. 到達性(Reachability)の検証: 関数の開始ノードから矢印を辿っていき、一度も辿り着けなかったブロックが存在した場合、型チェッカーが「Unreachable code(到達不能コード)」として警告・エラーを発生させます。

Hackの型チェッカー(OCamlで記述されています)は、共有メモリと高度な並列処理を駆使して、この巨大なグラフ計算をプロジェクト全体で一瞬にして完了させているのです。

—

3. 型システムと連動する「ボトム型(nothing / noreturn)」

Hackのデッドコード解析の美しいところは、「制御フロー」と「型システム」が完全に融合している点にあります。

`noreturn` 型:決して戻らない関数の印

例外を投げる関数や、プロセスを終了させる関数には `noreturn` という特殊な戻り値型を指定します。

function fail_with_log(string $message): noreturn {
// ログ出力処理…
throw new Exception($message);
}

function process_order(?string $item_id): string {
if ($item_id is null) {
fail_with_log(“Item ID cannot be null!”);
// 型チェッカーは「fail_with_logが値を返さない」ことを知っている
}

// ここに来た時点で、$item_id は確実に string 型(nullは排除されている)
return “Processing: “.$item_id;
}

型チェッカーは `fail_with_log` が `noreturn` であることを理解しているため、その呼び出し以降のパスを切断します。その結果、直後の `$item_id` は型の絞り込み(Type Refinement)によって、`?string` から `string` へと安全に昇格できるわけですね。

`nothing` 型:値が存在し得ない極限の型

もし型の絞り込みが進みすぎて「どんな値も入り得ない状態」になった場合、その変数の型は `nothing`(ボトム型) になります。

function check_number(int $value): void {
if ($value > 10) {
if ($value < 5) { // 10より大きくて 5より小さい int は存在しない! // この時点で $value の型は `nothing` になる echo "ここは絶対に通りません"; // ← デッドコードエラー! } } } 条件矛盾によって変数が `nothing` 型になった瞬間、型チェッカーはその先がデッドコードであると論理的に断定します。 ---

4. 実践コードで見る!デッドコード検知の具体例

実際の開発で遭遇しやすいパターンを2つ見てみましょう。

ケース1: 列挙型(enum)の網羅的チェックと不要な `default`

Hackでは `enum` と `switch` を組み合わせた網羅性チェックが強力です。すべてのケースを処理している場合、不要な `default` はデッドコードになります。

enum Status: string {
PENDING = ‘pending’;
APPROVED = ‘approved’;
REJECTED = ‘rejected’;
}

function get_status_badge(Status $status): string {
switch ($status) {
case Status::PENDING:
return ‘badge-yellow’;
case Status::APPROVED:
return ‘badge-green’;
case Status::REJECTED:
return ‘badge-red’;

// 罠: すでに全パターン網羅されているため、default は不要!
default:
// エラー: このブロックには到達しません
return ‘badge-gray’;
}
}

全ケースがカバーされていれば、`switch` を抜けた後に到達することはありません。Hackは「将来新しいステータスが追加されたら、`switch` に書き忘れた時点でエラーにする」という思想を持っているため、不要な `default` をあえて書かないのがベストプラクティスです。

ケース2: ループ内部の早期脱出(`break` や `continue` の後)

function search_user(vec $users, string $target): bool {
foreach ($users as $user) {
if ($user === $target) {
return true;
$found_count++; // ← エラー: return の後ろにコードがある
}
}
return false;
}

リファクタリングの過程でコードを移動した際、うっかり残ってしまった残骸も型チェッカーが見逃さずに検知してくれます。

—

5. 陥りやすい罠と型チェッカーとの付き合い方

開発中に「デッドコード検知エラー」が出たときは、以下のポイントをチェックしてみてください。

1. `throw` や `return` の直後に処理を書いていないか?

  • ログ出力や後処理を `return` の前に移動しましょう。

2. 型のガード条件が矛盾していないか?

  • `if ($x is int && $x is string)` のような論理的にあり得ない条件分岐がないか見直しましょう。

3. `invariant()` 関数を正しく活用できているか?

  • 「ここは絶対に通らないはず」という場所には、コメントではなく `invariant_violation(‘理由’)` を書くことで、型チェッカーにも「ここは例外パスである」と明示的に伝えることができます。

function process_data(mixed $data): void {
if ($data is int) {
// 数値の処理
} else if ($data is string) {
// 文字列の処理
} else {
// 想定外の型が来た場合は明示的に落とす
invariant_violation(‘Expected int or string, got %s’, gettype($data));
}
}

—

まとめ

HHVM型チェッカーのデッドコード解析は、単なる「お小言」ではありません。
「制御フローグラフ(CFG)」と「厳密な型システム」が美しく協調し、実行時エラーやロジックの破綻を未然に防ぐための強力なセーフティネットです。

  • CFG によって、すべての実行パスの繋がりを常に計算している
  • `noreturn` や `nothing` を使って、ロジックの矛盾を型のレベルで捉える
  • デッドコードを削ぎ落とすことで、コードベースの純度と保守性が極限まで高まる

型チェッカーから「到達不能コードがあるよ」と怒られたら、「バグになる前に教えてくれてありがとう!」と前向きに受け止めてコードを綺麗に整えてみてくださいね。

Hackの静的解析を味方につければ、大規模開発でも怖くありません。一歩ずつマスターしていきましょう!

タイトルとURLをコピーしました