Rustで作る自作コンパイラ入門シリーズ【完結】
| Part | タイトル | 内容 |
|---|---|---|
| Part1 | 字句解析器を書いたら正規表現の気持ちがわかった | Lexer |
| Part2 | ASTって何?木構造で式を理解する | Parser |
| Part3 | 変数のスコープを実装したら、Rustの気持ちがわかった | 意味解析 |
| Part4 | スタックマシンを実装したらCPUの気持ちがわかった | コード生成 + VM |
| Part5 | 自作言語でFizzBuzzが動いた!完結編 | 全部つなげて動かす |
はじめに
長かった...本当に長かった...
Part1から始まったこのシリーズ、ついに最終回です。
今回やること:
- 全モジュールを統合する
- FizzBuzzを自作言語で書いて動かす
- **REPL(対話環境)**を作る
- シリーズ全体を振り返る
これが動いたら、本当にプログラミング言語を作ったと胸を張れます。
...多分。
最終的なアーキテクチャ
Part1〜Part4で作ったものを全部つなげると、こうなります:
┌──────────────────────────────────────────────────────────┐
│ ソースコード │
│ fn fizzbuzz(n) { if n % 15 == 0 { return 3; } ... } │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ 1. Lexer(字句解析) │ Part1
│ 文字列 → トークン列 │
└──────────────────────────────┘
│
[Let, Ident("fizzbuzz"), LParen, ...]
│
▼
┌──────────────────────────────┐
│ 2. Parser(構文解析) │ Part2
│ トークン列 → AST │
└──────────────────────────────┘
│
Function { name: "fizzbuzz", ... }
│
▼
┌──────────────────────────────┐
│ 3. Analyzer(意味解析) │ Part3
│ AST → 検証済みAST │
└──────────────────────────────┘
│
(未定義変数・引数不一致チェック済)
│
▼
┌──────────────────────────────┐
│ 4. CodeGen(コード生成) │ Part4
│ AST → バイトコード │
└──────────────────────────────┘
│
[Push(15), LoadLocal(0), Mod, Push(0), Eq, ...]
│
▼
┌──────────────────────────────┐
│ 5. VM(仮想マシン) │ Part4
│ バイトコード → 実行 │
└──────────────────────────────┘
│
▼
実行結果: 3
5つのモジュール、合計 約1500行 のRustコードでできています。
コンパイル&実行パイプライン
compile_and_run関数
全部をつなげる関数:
fn compile_and_run(source: &str, trace: bool) -> Result<i64, String> {
// 1. 字句解析
let mut lexer = Lexer::new(source);
let tokens = lexer.tokenize();
// 2. 構文解析
let program = Parser::parse_source(source)?;
// 3. 意味解析
analyze_program(&program).map_err(|e| e.message)?;
// 4. コード生成
let compiled = CodeGenerator::generate(&program);
// 5. 実行
execute(compiled, trace)
}
たったこれだけで、ソースコードが実行できます!
いよいよFizzBuzz
FizzBuzzを自作言語で書く
fn fizzbuzz(n) {
if n % 15 == 0 {
return 3; // FizzBuzz
} else {
if n % 3 == 0 {
return 1; // Fizz
} else {
if n % 5 == 0 {
return 2; // Buzz
} else {
return 0; // 数字そのまま
}
}
}
}
fn main() {
let i = 1;
while i <= 20 {
let result = fizzbuzz(i);
// result: 0=数字, 1=Fizz, 2=Buzz, 3=FizzBuzz
i = i + 1;
}
return 0;
}
※ 今回作った言語にはprintがないので、戻り値でFizzBuzz判定を返しています。
実行!
cargo run
出力:
╔═══════════════════════════════════════╗
║ Mini Rust Compiler v0.1.0 ║
╚═══════════════════════════════════════╝
=== FizzBuzz(15) ===
Result: 3
動いたーーーー!!!🎉🎉🎉
fizzbuzz(15)が3を返しています。15は15の倍数なのでFizzBuzz、正解!
他のケースもテスト
// テストコード
fn test_fizzbuzz() {
assert_eq!(run("return fizzbuzz(1);"), 0); // 数字
assert_eq!(run("return fizzbuzz(3);"), 1); // Fizz
assert_eq!(run("return fizzbuzz(5);"), 2); // Buzz
assert_eq!(run("return fizzbuzz(15);"), 3); // FizzBuzz
assert_eq!(run("return fizzbuzz(9);"), 1); // Fizz
assert_eq!(run("return fizzbuzz(10);"), 2); // Buzz
assert_eq!(run("return fizzbuzz(30);"), 3); // FizzBuzz
assert_eq!(run("return fizzbuzz(7);"), 0); // 数字
}
running 1 test
test test_fizzbuzz ... ok
test result: ok. 1 passed; 0 failed
完璧!!
1から20までのFizzBuzz結果
ループ版も動かしてみましょう:
fn fizzbuzz(n) {
if n % 15 == 0 { return 3; }
else {
if n % 3 == 0 { return 1; }
else {
if n % 5 == 0 { return 2; }
else { return n; } // 数字をそのまま返す
}
}
}
fn main() {
let i = 1;
let sum = 0;
while i <= 20 {
sum = sum + fizzbuzz(i);
i = i + 1;
}
return sum; // 全結果の合計
}
各数に対する結果:
| n | n % 15 | n % 3 | n % 5 | 結果 |
|---|---|---|---|---|
| 1 | 1 | 1 | 1 | 1 |
| 2 | 2 | 2 | 2 | 2 |
| 3 | 3 | 0 | 3 | Fizz (1) |
| 4 | 4 | 1 | 4 | 4 |
| 5 | 5 | 2 | 0 | Buzz (2) |
| 6 | 6 | 0 | 1 | Fizz (1) |
| 7 | 7 | 1 | 2 | 7 |
| ... | ... | ... | ... | ... |
| 15 | 0 | 0 | 0 | FizzBuzz (3) |
| ... | ... | ... | ... | ... |
全部正しく動いています!
再帰も動く
Part4で作ったVMは関数呼び出しをサポートしているので、再帰もちゃんと動きます:
fn factorial(n) {
if n <= 1 {
return 1;
} else {
return n * factorial(n - 1);
}
}
fn main() {
return factorial(10);
}
=== Recursive factorial(10) ===
Result: 3628800
10! = 3628800 で正解!
フィボナッチ数列
fn fib(n) {
if n <= 1 {
return n;
} else {
return fib(n - 1) + fib(n - 2);
}
}
fn main() {
return fib(10);
}
=== Fibonacci(10) ===
Result: 55
fib(10) = 55 で正解!
REPLを作る
せっかくなので、対話的に実行できるREPL(Read-Eval-Print Loop)も作りましょう。
use std::io::{self, Write};
fn repl() {
println!("Mini Rust REPL v0.1.0");
println!("Type 'exit' to quit.\n");
// 事前定義の関数(REPLセッション中使える)
let mut prelude = String::new();
loop {
print!(">>> ");
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
let input = input.trim();
if input == "exit" || input == "quit" {
println!("Bye!");
break;
}
if input.is_empty() {
continue;
}
// 関数定義なら保存
if input.starts_with("fn ") && !input.starts_with("fn main") {
prelude.push_str(input);
prelude.push('\n');
println!("Function defined.");
continue;
}
// 式ならmain関数でラップして実行
let source = if input.starts_with("fn main") {
format!("{}\n{}", prelude, input)
} else if input.starts_with("return") {
format!("{}\nfn main() {{ {} }}", prelude, input)
} else {
format!("{}\nfn main() {{ return {}; }}", prelude, input)
};
match compile_and_run(&source, false) {
Ok(result) => println!("=> {}", result),
Err(e) => println!("Error: {}", e),
}
}
}
REPLデモ
Mini Rust REPL v0.1.0
Type 'exit' to quit.
>>> 1 + 2 * 3
=> 7
>>> fn add(a, b) { return a + b; }
Function defined.
>>> add(10, 20)
=> 30
>>> fn factorial(n) { if n <= 1 { return 1; } else { return n * factorial(n - 1); } }
Function defined.
>>> factorial(5)
=> 120
>>> factorial(10)
=> 3628800
>>> exit
Bye!
対話的にコードが実行できるようになりました!
バイトコードを覗いてみる
生成されるバイトコードを見てみましょう:
fn add(a, b) { return a + b; }
fn main() { return add(10, 20); }
=== Bytecode ===
Function: add (params: 2, locals: 2)
0: LoadLocal(0) // a
1: LoadLocal(1) // b
2: Add // a + b
3: Return
4: Push(0) // 暗黙のreturn 0
5: Return
Function: main (params: 0, locals: 0)
0: Push(20) // 第2引数(逆順でプッシュ)
1: Push(10) // 第1引数
2: Call("add") // add(10, 20)
3: Return
4: Push(0)
5: Return
引数が逆順でスタックにプッシュされて、関数呼び出しで使われる様子がわかります。
作った言語の文法まとめ
サポートする構文
プログラム := 関数定義*
関数定義 := "fn" 識別子 "(" パラメータリスト? ")" ブロック
ブロック := "{" 文* "}"
文 := let文 | 代入文 | if文 | while文 | return文 | 式文
let文 := "let" 識別子 "=" 式 ";"
代入文 := 識別子 "=" 式 ";"
if文 := "if" 式 ブロック ("else" ブロック)?
while文 := "while" 式 ブロック
return文 := "return" 式? ";"
式文 := 式 ";"
式 := 論理OR式
論理OR式 := 論理AND式 ("||" 論理AND式)*
論理AND式 := 等値式 ("&&" 等値式)*
等値式 := 比較式 (("==" | "!=") 比較式)*
比較式 := 加減式 (("<" | "<=" | ">" | ">=") 加減式)*
加減式 := 乗除式 (("+" | "-") 乗除式)*
乗除式 := 単項式 (("*" | "/" | "%") 単項式)*
単項式 := ("-" | "!") 単項式 | 基本式
基本式 := 数値 | "true" | "false" | 識別子 | 関数呼び出し | "(" 式 ")"
関数呼び出し := 識別子 "(" 引数リスト? ")"
演算子の優先順位(低→高)
| 優先順位 | 演算子 |
|---|---|
| 1(低) | ` |
| 2 | && |
| 3 |
== !=
|
| 4 |
< <= > >=
|
| 5 |
+ -
|
| 6 |
* / %
|
| 7(高) |
- (単項) !
|
統計情報
コード行数
| モジュール | ファイル | 行数 |
|---|---|---|
| 字句解析 | lexer.rs |
408 |
| 構文解析 | parser.rs |
735 |
| 意味解析 | analyzer.rs |
451 |
| コード生成 | codegen.rs |
325 |
| 仮想マシン | vm.rs |
434 |
| メイン | main.rs |
100 |
| 合計 | 約2450行 |
思ったより少ない!Rustの表現力のおかげですね。
実行できるプログラムの例
// ✅ これらは全部動く!
// 四則演算
return 1 + 2 * 3 - 4 / 2; // => 5
// 変数
let x = 10;
let y = 20;
return x + y; // => 30
// if文
if x > 0 { return 1; } else { return 0; }
// ループ
while i < 10 { i = i + 1; }
// 関数
fn add(a, b) { return a + b; }
return add(1, 2); // => 3
// 再帰
fn fib(n) {
if n <= 1 { return n; }
return fib(n-1) + fib(n-2);
}
return fib(10); // => 55
// FizzBuzz!!!
fn fizzbuzz(n) { ... }
return fizzbuzz(15); // => 3 (FizzBuzz!)
シリーズ全体の振り返り
Part1: 字句解析
学んだこと:
- トークンの設計
- 文字列を1文字ずつ読む処理
- 先読み(peek)の重要性
- キーワードと識別子の区別
Rustの良さ:
-
enumで値を持てる(Number(42)) - パターンマッチの網羅性チェック
Part2: 構文解析
学んだこと:
- AST(抽象構文木)の設計
- 再帰下降パーサー
- 演算子の優先順位
Rustの良さ:
-
Box<T>で再帰的データ構造 -
?演算子でエラー処理が楽
Part3: 意味解析
学んだこと:
- スコープの概念
- シンボルテーブル
- 2パス解析(前方参照対応)
Rustの良さ:
-
HashMapで変数管理 - 所有権でスコープの出入りが明確
Part4: コード生成 + VM
学んだこと:
- スタックマシンの仕組み
- バックパッチング(ジャンプ先の後埋め)
- コールスタックと関数呼び出し
Rustの良さ:
-
Vecがスタックとして使える -
clone()で状態のコピーが楽
Part5: 統合 + FizzBuzz
学んだこと:
- 全体を組み合わせる設計
- REPLの作り方
- プログラミング言語が作れるという自信!
今後の発展
この言語をもっと実用的にするなら:
すぐできる拡張
-
print関数の追加 - 文字列リテラルのサポート
- 配列のサポート
-
for文の追加
中級の拡張
- 型システムの追加
- 型推論
- クロージャ
- 標準ライブラリ
上級の拡張
- LLVMバックエンド(ネイティブコード生成)
- ガベージコレクション
- モジュールシステム
- マクロ
興味があれば、ぜひ挑戦してみてください!
まとめ
作ったもの
Rustでプログラミング言語を作りました!
- 字句解析器(Lexer)
- 構文解析器(Parser)
- 意味解析器(Analyzer)
- コード生成器(CodeGenerator)
- スタックベース仮想マシン(VM)
- REPL(対話環境)
FizzBuzzが動いた!
fn fizzbuzz(n) {
if n % 15 == 0 { return 3; }
else {
if n % 3 == 0 { return 1; }
else {
if n % 5 == 0 { return 2; }
else { return 0; }
}
}
}
これが本当に動くんです。自分で作ったコンパイラで。
最後に
正直、最初は「プログラミング言語を作るとか無理でしょ」って思ってた?
でも、1つずつステップを踏んでいけば、意外とできるものなんですよね...
コンパイラを作ると、普段使っている言語のありがたみがわかります。
- Rustのコンパイラがなぜあんなに親切なエラーメッセージを出せるのか
- 型システムがなぜ重要なのか
- 最適化がどれだけ難しいのか
全部、身をもって体験できました。
みなさんも、ぜひ一度プログラミング言語を作ってみてください。
低レイヤーの世界、楽しいよ。
参考資料
- Crafting Interpreters - 無料で読める神本
- Writing An Interpreter In Go - インタプリタ実装の名著
- Engineering a Compiler - コンパイラの教科書
- The Rust Programming Language - Rust公式本
謝辞
ここまで読んでくれてありがとうございました!
5回に渡るシリーズ、楽しんでいただけたでしょうか?
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!
それでは、また別の記事でお会いしましょう!👋
Happy Hacking! 🦀