コンパイラ
コンパイラ
人間が書いたコードを機械が実行できる形へ翻訳するプログラム。インタプリタと対をなす。
概要
コンパイラは、人間が読み書きするプログラミング言語(ソースコード)を、CPU が直接実行できる機械語、あるいはそれに近い中間形式へ「翻訳」するプログラムです。CPU が理解できるのは2進数で表された命令だけであり、if や for といった人間向けの表現をそのまま実行することはできません。この隔たりを埋める翻訳者がコンパイラです。
翻訳のタイミングには流儀があります。実行前にまとめて全部翻訳しておくのがコンパイル方式(C・Rust・Go など)、実行しながらその場で解釈していくのがインタプリタ方式(初期の Python・Ruby など)です。さらに現代では、実行しながら「よく使われる部分だけ」を機械語に翻訳する JIT(Just-In-Time)コンパイルが両者のいいとこ取りとして主流になり、JavaScript や Java の高速さを支えています。普段コンパイラを意識しない開発者でも、ビルドエラー・型エラー・最適化といった形で毎日その恩恵と対話しています。
なぜ生まれたか
コンピュータ黎明期、プログラムは機械語かそれに1対1対応するアセンブリ言語で書かれていました。数値の羅列に近いコードは書くのも読むのも修正するのも苦行で、しかも CPU の種類ごとに全部書き直しです。「人間の思考に近い記法で書き、機械への翻訳は機械にやらせればよい」— この発想を実用にしたのが、1957年に IBM の John Backus らが開発した FORTRAN コンパイラです。当時は「機械が翻訳したコードなど手書きのアセンブリより遅いに決まっている」と懐疑されましたが、FORTRAN は最適化によって手書きに迫る性能を示し、高水準言語の時代を開きました。
コンパイラの発明が変えたのは生産性だけではありません。「ソースコードと実行形式の分離」により、同じソースコードを異なる CPU 向けにコンパイルし直せば移植できるようになり、ソフトウェアがハードウェアから独立した資産になりました。また、翻訳という工程を挟むことで、実行する前にプログラムの誤りを機械的に検査する余地 — 後述する静的型検査など — が生まれました。
詳細
コンパイラの中身 — 4つの段階
コンパイラの内部は、おおまかに4つの段階のパイプラインです。まず字句解析で、文字の並びを「キーワード」「識別子」「演算子」といった意味の最小単位(トークン)に分割します。次に構文解析で、トークンの並びを言語の文法に照らして木構造(抽象構文木、AST)に組み上げます。文法違反はここでシンタックスエラーとして検出されます。続く最適化では、意味を変えずにコードを速く・小さく変形します — 定数計算の事前実行、使われない・到達しないコードの削除、ループの変形、関数のインライン展開などです。最後にコード生成で、ターゲット CPU の機械語(または中間形式)を出力します。現代の代表的な基盤である LLVM は、この後半部分(最適化とコード生成)を共通化したもので、Rust も Swift も Clang もフロントエンドだけを作って LLVM に接続しています。
実行方式の比較 — コンパイル・インタプリタ・JIT
事前コンパイルは実行時が最速ですが、動かすまでにビルドの待ち時間があり、配布物は CPU・OS ごとに作り分けが必要です。インタプリタは書いたそばから動く手軽さと移植性が魅力ですが、実行のたびに解釈コストを払うため遅くなりがちです。JIT はまずインタプリタとして即座に実行を開始し、実行中に「何度も呼ばれる熱い(hot)部分」を検出して、その部分だけを機械語へコンパイルします。
JIT の面白さは「実行しながらだからこそできる最適化」にあります。JavaScript の V8 エンジンが好例です。JavaScript は動的型付けで、変数の型がコンパイル時には分かりません。しかし V8 は実行を観察し、「この関数の引数はこれまで常に整数だった」という実測に基づいて、整数専用の高速な機械語を生成します(投機的最適化)。もし後で文字列が渡ってきたら、その機械語を捨てて解釈実行に戻る(脱最適化)ことで正しさを守ります。静的な情報だけでは不可能な、実行プロファイルに基づく賭けと巻き戻し — これが「スクリプト言語は遅い」という常識を覆した仕組みです。
コンパイル時に誤りを捕まえる — 静的型検査
翻訳工程を挟むことの大きな恩恵が、実行する前の検査です。静的型付き言語のコンパイラは、構文解析の後に「この関数は数値を受け取るはずなのに文字列が渡されている」といった型の矛盾を検査し、誤りをコンパイルエラーとして突き返します。実行してみるまで分からないバグの一群が、書いた直後に機械的に検出される — テストを書く前の第一の防衛線です。TypeScript の人気は、動的型付けの JavaScript にこのコンパイル時検査を後付けした点にあり、Rust に至っては型検査の枠組みを拡張してメモリ安全性(ガベージコレクションなしの安全なメモリ管理)までコンパイル時に証明します。「実行時の失敗をコンパイル時の失敗に前倒しする」ことは、言語設計の一大潮流です。
広がるコンパイラの世界 — トランスパイラとWebAssembly
翻訳先は機械語だけではありません。高水準言語から別の高水準言語へ変換するコンパイラはトランスパイラと呼ばれ、TypeScript から JavaScript への変換や、新しい JavaScript 構文を古いブラウザ向けに書き下げる Babel が代表です。フロントエンド開発で使うバンドラも、この変換パイプラインの上に成り立っています。また、ブラウザという実行環境に向けた共通のコンパイル先として設計されたのが WebAssembly で、C++ や Rust のコンパイラが Wasm を出力することで、ブラウザ上でネイティブに迫る速度のコードが動くようになりました。「あらゆる言語 → 共通の中間形式 → あらゆる実行環境」という構図は、まさに FORTRAN 以来コンパイラが担ってきた「書く場所と動く場所の分離」の現代版と言えます。