Skip to content

コンパイラ入門

はじめに

「実行」ボタンを押したとき、コードはどうやって画面に結果を表示するのか? あなたが書いたコードの一行一行を、コンピュータは実は「理解できません」——コンピュータが認識できるのは 0 と 1 だけです。コンパイラとは、人間の言語を機械語に翻訳する「翻訳者」です。コンパイラの原理を理解すれば、エラーメッセージがどこから来るのか、なぜある言語は速くてある言語は遅いのか、そしてコード最適化の基盤となるロジックを理解できるようになります。

この記事で学べること:

この章を読み終えると、以下の知識が得られます:

  • 全体像:ソースコードから実行可能プログラムまでの完全なコンパイルパイプラインを把握する
  • 字句解析:コンパイラがコードをトークンに分割する仕組みを理解する
  • 構文解析:AST(抽象構文木)の構築プロセスを理解する
  • AST の可視化:コードの木構造を直感的に見る
  • 意味解析と最適化:型チェックとコード最適化の原理を理解する
  • 最適化技術の実践:定数畳み込み、デッドコード除去など中核的な最適化手法を習得する
  • 実行モデル:コンパイル型、インタプリタ型、JIT の 3 つの実行方式を区別する
内容コアコンセプト
第 1 章コンパイラとは翻訳者のアナロジー、コンパイルパイプライン
第 2 章字句解析トークン、字句規則
第 3 章構文解析AST、構文木、優先順位
第 4 章AST の可視化インタラクティブな構文木、ノードタイプ
第 5 章意味解析と最適化型チェック、定数畳み込み、デッドコード除去
第 6 章最適化技術の実践関数インライン展開、ループ外への移動、定数伝播
第 7 章コンパイル型 vs インタプリタ型 vs JIT3 つの実行モデルの比較

0. 全体像:コードの「翻訳の旅」

あなたが翻訳者で、中国語の小説を英語に翻訳する場面を想像してください。一文字ずつ直訳するのではなく、次のように進めます:

  1. 単語を識別する — 文章を単語ごとに分割する(字句解析)
  2. 構文を理解する — 文の構造が正しいか判断する(構文解析)
  3. 意味を理解する — 意味が通じ、矛盾がないことを確認する(意味解析)
  4. 推敲して最適化する — 訳文をより自然で流暢にする(コード最適化)
  5. 訳文を出力する — 最終的な英語版を書き出す(コード生成)

コンパイラの仕事もまったく同じです。ただし翻訳する対象がプログラミング言語であるというだけの違いです。

Compiler Principles: The Art of TranslationHow code becomes machine instructions
A compiler is like a translator, turning human-readable code into machine-readable instructions
The Complete Code Translation Pipeline
1
Lexical analysis
Break code into individual words called tokens
int age = 25 → [int, age, =, 25]
2
Syntax analysis
Check grammar rules and build a syntax tree
Validate whether statement structure is correct
3
Semantic analysis
Check whether the meaning of the code is valid
Check variable definitions and type compatibility
4
Intermediate code generation
Generate a machine-independent intermediate representation
Generate bytecode or intermediate representation
5
Optimization
Improve code so it runs more efficiently
Constant folding and dead-code elimination
6
Target code generation
Generate machine code or target code
Generate x86 or ARM machine instructions
Lexical analysis: tokenization
int age = 25;
Keywordint
Identifierage
Operator=
Number25
Separator;
Syntax analysis: build a tree
Assignment statement
Variableage
Operator=
Number25
Compilation vs Interpretation
Compiled languages
Source code → Compiler → Machine code
C, Go, Rust
✓ Fast execution
✓ Compile once, run many times
✗ Slow compile step
Interpreted languages
Source code → Interpreter → Line-by-line execution
Python, JavaScript, PHP
✓ Fast development
✓ Cross-platform
✗ Slower execution
Compiler Optimization
Before:
x = 5 + 3 + 2
⬇️
After:
x = 10
The compiler can optimize code automatically and improve runtime efficiency

1. コンパイラの 6 段階パイプライン

コンパイラの仕事は 6 つの段階に分けられます。工場の生産ラインのように、各段階が処理を終えると次の段階に引き渡します。

How a Compiler WorksA six-step journey from source code to machine code
1
Lexical analysis→ Token stream
2
Syntax analysis→ AST syntax tree
3
Semantic analysis→ Typed AST
4
Intermediate code generation→ IR (intermediate representation)
5
Code optimization→ Optimized IR
6
Target code generation→ Machine code
1Lexical analysisOutput: Token stream
Split source code into individual words called tokens, like recognizing each word in a sentence.
Recognize keywordsRecognize identifiersRecognize numbersRecognize operatorsFilter whitespace
int x = 10 + 5;
→ [int] [x] [=] [10] [+] [5] [;]
    keyword identifier operator number operator number separator
Live lexical analysis
intKeyword
xIdentifier
=Operator
10Number
+Operator
5Number
;Separator
Three Execution Models Compared
Compiled
Source Compiler Machine code CPU execution
Fast executionMust wait for compilation
C, C++, Rust, Go
Interpreted
Source Interpreter Line-by-line execution
Run immediately while writingSlower execution
Python, Ruby, PHP
JIT
Source Bytecode JIT hot path compilation Execution
Balances performance and flexibilitySlower startup
Java, JavaScript (V8)
Core idea:A compiler is like a translator: it gradually turns human-readable code into instructions the machine can run. The six stages each do one job: identify words → understand syntax → check meaning → generate IR → optimize → generate machine code.

コンパイルパイプライン

  1. 字句解析(Lexical Analysis):ソースコードをトークン(単語)に分割する
  2. 構文解析(Syntax Analysis):トークンを構文木(AST)に整理する
  3. 意味解析(Semantic Analysis):型が正しいか、変数が宣言されているかをチェックする
  4. 中間コード生成(IR Generation):プラットフォームに依存しない中間表現を生成する
  5. コード最適化(Optimization):中間コードをより効率的にする
  6. コード生成(Code Generation):ターゲットプラットフォームの機械語を生成する
段階入力出力アナロジー
字句解析ソースコード文字ストリームトークンストリーム文章を単語に分割する
構文解析トークンストリームAST(構文木)文の構造を分析する
意味解析AST型付き AST意味が通じるかチェックする
中間コード型付き ASTIR下書きを書く
コード最適化IR最適化された IR推敲して削減する
コード生成最適化された IR機械語最終稿を出力する

2. 字句解析:コードを「単語」に分割する

字句解析はコンパイルの第一歩です。コンパイラはソースコードの各文字を左から右へスキャンし、それらを意味のあるトークン(字句単位)にまとめます。

🔤 Lexer: Split Code into Tokens

Enter a line of code and see lexical analysis results in real time

英文を読むときに脳が自動的に文字を単語にまとめるのと同じように、字句解析器は文字をトークンにまとめます:

ソースコード: let x = 10 + 5;

トークンストリーム:
[let]   → キーワード(言語の予約語)
[x]     → 識別子(変数名)
[=]     → 演算子(代入)
[10]    → 数値リテラル
[+]     → 演算子(加算)
[5]     → 数値リテラル
[;]     → 区切り文字(文の終了)

トークンの 5 大タイプ

  • キーワード:言語が予約する特別な単語。例: letifreturnfunction
  • 識別子:プログラマが定義する名前。例: 変数名、関数名
  • リテラル:コードに直接書かれた値。例: 数値 42、文字列 "hello"
  • 演算子:演算を実行する記号。例: +-====
  • 区切り文字:コード構造を区切る記号。例: ;,()

3. 構文解析:構文木(AST)を構築する

字句解析はコードをトークンに分割しましたが、トークンはまだ孤立した「単語」に過ぎません。構文解析の役割は、これらのトークンを文法規則に従って抽象構文木(Abstract Syntax Tree, AST)に整理することです。AST はコードの構造と演算の優先順位を反映します。

式: 1 + 2 * 3

構文木:          なぜこうなる?
       +         * の優先順位が
      / \        + より高いため、
     1   *       2 * 3 が先に
        / \      結合されて
       2   3     部分木になる

AST の重要性

AST はコンパイラの「中核的データ構造」であり、後続の意味解析、最適化、コード生成はすべて AST に基づいて行われます。現代の開発ツールも AST を多用しています:

  • ESLint:コードを AST に解析し、ルール違反がないかチェックする
  • Prettier:AST に解析した後、再フォーマットして出力する
  • Babel:AST を解析 → 変換 → 互換コードを生成する
  • IDE のリファクタリング:AST に基づいて安全な変数名変更や関数抽出を行う
構文構造トークン列AST ノード
変数宣言let x = 10VariableDeclaration → Identifier + Literal
関数呼び出しadd ( 1 , 2 )CallExpression → Identifier + Arguments
条件文if ( a > b )IfStatement → BinaryExpression + Block

4. AST の可視化:コードの「骨組み」を見る

ここまで AST の構造を文章で説明してきましたが、「読む」よりも「見る」方が直感的です。以下のインタラクティブコンポーネントでは、さまざまな式を選択して、その構文木がどのような形になるかをリアルタイムで観察できます。

🌳 AST Visualizer: See the Skeleton of Code

Choose an expression and inspect its abstract syntax tree

Syntax tree
BinaryExpression+
NumericLiteral1
BinaryExpression*
NumericLiteral2
NumericLiteral3
Parse notes
1* has higher precedence than +, so 2 * 3 groups first
22 * 3 forms a BinaryExpression subtree
31 and that subtree become the left and right operands of +
4The final + node is the root, showing the evaluation order
💡 Try AST Explorer — inspect ASTs for arbitrary code online

可視化を通じて、AST の核心的なルールが実はとてもシンプルであることがわかります:

コード構造AST ルートノード子ノード
1 + 2 * 3BinaryExpression (+)左: NumericLiteral(1)、右: BinaryExpression(*)
let x = 10VariableDeclarationVariableDeclarator → Identifier(x) + NumericLiteral(10)
add(a, b)CallExpressionIdentifier(add) + Arguments(a, b)

AST の日常開発での応用

あなたは直接コンパイラを書いたことがないかもしれませんが、AST ベースのツールを毎日使っています:

  • ESLint / Prettier:コードを AST に解析し、ルールチェックや再フォーマットを行う
  • Babel / SWC:AST を解析 → 構文を変換 → 互換コードを生成する
  • IDE のリファクタリング:AST に基づいて安全な名前変更や関数抽出を行う
  • Tree-shaking:AST 内の import/export を分析し、未使用コードを削除する

5. 意味解析とコード最適化

構文解析はコードの「構造が正しい」ことを保証しますが、構造が正しくても「意味が正しい」とは限りません。意味解析はコードの意味が合法かどうかをチェックし、コード最適化はプログラムをより速く実行させる役割を担います。

Compilation PracticeFrom code to executable file
Input code
Compilation steps
1
Preprocess
gcc -E hello.c -o hello.i
Process #include and expand macros
2
Compile
gcc -S hello.i -o hello.s
Generate assembly code
3
Assemble
gcc -c hello.s -o hello.o
Generate object file
4
Link
gcc hello.o -o hello
Generate executable file
Generated files
📄
hello.c
Source code file
📝
hello.i
Preprocessed file
⚙️
hello.s
Assembly code file
📦
hello.o
Object file
🚀
hello
Executable file
Common compiler tools
GCC
GNU Compiler Collection
Clang
LLVM C/C++ compiler
MSVC
Microsoft Visual C++

4.1 意味解析:「意味」が正しいかをチェックする

チェック内容結果
型チェックint x = "hello"❌ 型が一致しない
スコープチェック宣言されていない変数 y を使用❌ 変数が存在しない
型推論1 + 2.0✅ 推論結果は float
引数チェックadd(1, 2, 3) だが関数は 2 つの引数のみ受け付ける❌ 引数の数が一致しない

あなたが見たことのあるエラーのほとんどは、意味解析に由来します

  • TypeError: Cannot read properties of undefined — 型チェック
  • ReferenceError: x is not defined — スコープチェック
  • Expected 2 arguments, but got 3 — 引数チェック

4.2 コード最適化:プログラムをより速く

コンパイラは最終コードを生成する前に、中間コードに対してさまざまな最適化を施します。これらの最適化はプログラマからは見えませんが、パフォーマンスを大幅に向上させます。

最適化技術最適化前最適化後原理
定数畳み込みx = 10 + 5x = 15コンパイル時に直接結果を計算
デッドコード除去if (false) { ... }直接削除絶対に実行されないコード
定数伝播x = 15; y = x * 2y = 30既知の値を直接置換
ループ不変量の移動ループ内で毎回 len = arr.length を計算ループの外に移動重複計算を回避

6. 最適化技術の実践:コンパイラはどうコードを高速化するか

ここまでいくつかの最適化技術の名前を挙げてきましたが、ここからはコンパイラが具体的にどのようにそれを行うのかを深く見ていきます。以下のインタラクティブコンポーネントは、最も一般的な 5 種類のコンパイラ最適化を示しており、最適化前後のコードの違いを直感的に比較できます。

⚡ Compiler Optimization: Make Code Faster Automatically

Choose an optimization technique and see how the compiler improves code

📝 Before optimization
const width = 10
const height = 20
const area = width * height  // computed at runtime
console.log(area)
Compiler optimization
🚀 After optimization
const area = 200  // computed during compilation
console.log(200)
How Constant folding works
The compiler sees that width and height are constants, so it computes 10 * 20 = 200 during compilation. Runtime no longer needs a multiplication.
Performance gain:
30%

現代のコンパイラや JIT エンジン(V8、GCC、LLVM など)は、数十種類の最適化を自動的に適用します。開発者として、これらの最適化を手動で行う必要はありませんが、理解することで次のようなメリットがあります:

  • 最適化されやすいコードを書ける:たとえば let ではなく const を使うと、コンパイラが定数畳み込みを行いやすくなる
  • パフォーマンスの違いを理解できる:なぜ小さな関数は大きな関数より速いのか?コンパイラがそれらをインライン展開できるからです
  • 「逆最適化」を避けられるeval()with など、特定の書き方はコンパイラの最適化を妨げる
最適化技術発動条件パフォーマンスへの影響開発者ができること
定数畳み込み式中がすべて定数実行時計算を排除const 宣言を多用する
デッドコード除去コードが到達不能または結果が未使用コードサイズを削減不要なコードを適時に整理する
ループ不変量の移動ループ内に不変の計算がある重複計算を削減手動で抽出するのも良い習慣
関数インライン展開小さな関数が頻繁に呼び出される呼び出しオーバーヘッドを排除関数を小さく集中的に保つ
定数伝播変数の値がコンパイル時に確定可能計算チェーン全体が排除されるマジックナンバーの代わりに定数を使う

7. コンパイル型 vs インタプリタ型 vs JIT

コードを書き終えた後、それを実行するための 3 つの「翻訳方式」があります。この 3 つにはそれぞれ長所と短所があり、言語のパフォーマンス特性と使用シーンを直接決定します。

🔄 Compiled vs Interpreted vs JIT

Click an execution mode to see how code moves from source to running program

📝
Source code
main.c
⚙️
Compiler
Full compilation
📦
Machine code
Binary executable
🚀
Run directly
CPU runs it directly
Run speed
Very fast
Startup
Slow; compile first
Portability
Recompile required
Representative languages:CC++RustGo
次元コンパイル型インタプリタ型JIT 即时コンパイル
プロセス先に全量を機械語にコンパイルしてから実行読みながら実行し、行ごとに翻訳まずインタプリタ実行し、ホットコードを後でコンパイル
実行速度最速最遅中程度(ホットコードはコンパイル型に近い)
起動速度遅い(コンパイルが必要)速い(直接実行)中程度(ウォームアップが必要)
クロスプラットフォーム再コンパイルが必要ネイティブにクロスプラットフォームクロスプラットフォーム
代表的な言語C, Rust, GoPython, RubyJavaScript (V8), Java

なぜ JavaScript はこんなに速いのか?

V8 エンジンの JIT コンパイラは、どのコードが頻繁に実行されているか(ホットコード)を監視し、それらを高度に最適化された機械語にコンパイルします。そのため、JavaScript は「インタプリタ型言語」でありながら、V8 上ではコンパイル型言語に近いパフォーマンスを発揮できます。これが Node.js をサーバーサイドで使える理由でもあります。


まとめ

コンパイラの原理は、コンパイラ開発者だけが知っていればよい知識ではありません。コンパイルプロセスを理解することで、エラーメッセージをよりよく理解し、適切な言語を選び、より効率的なコードを書けるようになります。

本章の重要ポイントを振り返ります:

  1. コンパイラは翻訳者:人間が読めるコードを機械が実行できる命令に翻訳する
  2. 6 段階のパイプライン:字句解析 → 構文解析 → 意味解析 → 中間コード → 最適化 → コード生成
  3. 字句解析でトークンに分割:文字ストリームをキーワード、識別子、演算子などの意味のある単位に分割する
  4. 構文解析で AST を構築:文法規則に従ってトークンを木構造に整理し、演算の優先順位を反映する
  5. 意味解析で正しさを保証:型チェック、スコープチェック。あなたが見たことのあるエラーのほとんどはここから来ている
  6. コンパイラが自動最適化:定数畳み込み、デッドコード除去、関数インライン展開などの技術でコードを自動的に高速化する
  7. 3 つの実行モデル:コンパイル型は最速、インタプリタ型は最も柔軟、JIT は両方の長所を兼ね備える

さらに学ぶ