3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rustで作る自作コンパイラ入門シリーズ【完結】

Part タイトル 内容
Part1 字句解析器を書いたら正規表現の気持ちがわかった Lexer
Part2 ASTって何?木構造で式を理解する Parser
Part3 変数のスコープを実装したら、Rustの気持ちがわかった 意味解析
Part4 スタックマシンを実装したらCPUの気持ちがわかった コード生成 + VM
Part5 自作言語でFizzBuzzが動いた!完結編 全部つなげて動かす

はじめに

長かった...本当に長かった...

Part1から始まったこのシリーズ、ついに最終回です。

今回やること:

  1. 全モジュールを統合する
  2. FizzBuzzを自作言語で書いて動かす
  3. **REPL(対話環境)**を作る
  4. シリーズ全体を振り返る

これが動いたら、本当にプログラミング言語を作ったと胸を張れます。

...多分。

最終的なアーキテクチャ

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のコンパイラがなぜあんなに親切なエラーメッセージを出せるのか
  • 型システムがなぜ重要なのか
  • 最適化がどれだけ難しいのか

全部、身をもって体験できました。

みなさんも、ぜひ一度プログラミング言語を作ってみてください。

低レイヤーの世界、楽しいよ。


参考資料

謝辞

ここまで読んでくれてありがとうございました!

5回に渡るシリーズ、楽しんでいただけたでしょうか?

この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!

それでは、また別の記事でお会いしましょう!👋

Happy Hacking! 🦀

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?