Chapter 2: 借用チェッカー詳細

この章の目標

この章を読み終えると、以下のことが理解できるようになります:

  • 借用チェッカーのコンパイルパイプライン上の位置
  • MIR(Mid-level IR)での解析過程
  • Non-Lexical Lifetimes (NLL) の仕組み
  • ローン(Loan)と制約生成のアルゴリズム
  • Polonius解析の改善点
  • エラーメッセージの生成プロセス
  • なぜ借用チェッカーを学ぶのか

    借用チェッカーは、Rustの所有権システムを実際に検証する機構です。その内部動作を理解することで:

  • エラーメッセージの真の意味が分かる
  • なぜコンパイルエラーになるのか予測できる
  • より効率的なコードを書ける
  • コンパイラの進化を追える
  • 1. 借用チェッカーの役割

    1.1 コンパイルパイプライン

    Rustのコンパイラ(rustc)は、複数の段階を経てコードを検証します:

    ソースコード(.rs)
      │
      ▼ Lexer/Parser
    AST(Abstract Syntax Tree)
      │ - 構文解析の結果
      │ - まだ型情報はない
      │
      ▼ Name Resolution
    HIR(High-level IR)
      │ - マクロ展開済み
      │ - 名前解決済み
      │
      ▼ Type Checking
    THIR(Typed HIR)
      │ - 型推論完了
      │ - 型エラーの検出
      │
      ▼ MIR Building
    MIR(Mid-level IR) ← 借用チェッカーはここで動作
      │ - 制御フローグラフ
      │ - 借用チェック
      │ - 最適化
      │
      ▼ Code Generation
    LLVM IR
      │
      ▼ LLVM Optimization & Code Generation
    機械語(バイナリ)
    

    1.2 MIR(Mid-level Intermediate Representation)

    MIRは、Rustコンパイラの中間表現で、借用チェッカーが動作する層です。

    1.2.1 MIRの特徴

    // ソースコード
    fn example() {
        let mut x = 0;
        let y = &mut x;
        *y += 1;
    }
    

    MIR(簡略化版)

    fn example() -> () {
        let mut _0: ();                  // 戻り値
        let mut _1: i32;                 // x
        let mut _2: &mut i32;            // y
        let mut _3: i32;                 // 一時変数
    
        bb0: {
            _1 = const 0_i32;            // x = 0
            _2 = &mut _1;                // y = &mut x
            _3 = (*_2);                  // *y を読む
            _3 = Add(move _3, const 1_i32); // _3 += 1
            (*_2) = move _3;             // *y = _3
            _0 = const ();               // return ()
            return;
        }
    }
    

    MIRの利点

  • 制御フローが明示的: Basic Blocks(bb0, bb1, ...)
  • 一時変数が明示的: _0, _1, _2, ...
  • 借用が明示的: &mut _1
  • 解析しやすい: 借用チェッカーに最適
  • 1.2.2 MIRの確認方法

    実際のMIRを確認できます:

    # MIRをダンプ
    rustc --emit=mir example.rs
    
    # または
    cargo rustc -- --emit=mir
    

    2. Non-Lexical Lifetimes (NLL)

    2.1 Lexical Lifetimes(Rust 2015)

    Rust 2015では、ライフタイムはスコープ(レキシカル)ベースでした:

    // Rust 2015: エラー
    fn example() {
        let mut s = String::from("hello");
        let r = &s;
        // rはここまで有効(スコープ全体)
        s.push_str(" world");  // エラー!rが存在する
        println!("{}", r);
    }
    

    問題点

  • スコープが終わるまで借用が続く
  • 実際に使わなくても借用は有効
  • 不必要に制限的
  • エラーメッセージ

    error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
     --> src/main.rs:4:5
      |
    3 |     let r = &s;
      |             -- immutable borrow occurs here
    4 |     s.push_str(" world");
      |     ^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
    5 |     println!("{}", r);
      |                    - immutable borrow later used here
    

    2.2 Non-Lexical Lifetimes(Rust 2018+)

    Rust 2018では、ライフタイムはデータフロー解析ベースになりました:

    // Rust 2018: OK
    fn example() {
        let mut s = String::from("hello");
        let r = &s;
        println!("{}", r);  // rの最後の使用
        // rはここで終了(スコープ前に)
    
        s.push_str(" world");  // OK!rはもう使われない
    }
    

    改善点

  • 参照の実際の使用期間を追跡
  • 最後の使用後は借用が終了
  • より自然なコードが書ける
  • 仕組み

    1. 制御フローグラフを構築
    2. 各参照の使用位置を追跡
    3. 最後の使用を特定
    4. 借用の終了位置を決定
    

    2.3 NLLの例

    例1: 条件分岐

    fn example(flag: bool) {
        let mut s = String::from("hello");
    
        if flag {
            let r = &s;
            println!("{}", r);
            // rはここで終了
        }
    
        s.push_str(" world");  // OK(ifの中でrは終了)
    }
    

    例2: ループ

    fn example() {
        let mut v = vec![1, 2, 3];
    
        for i in 0..v.len() {
            let x = &v[i];  // 不変借用
            println!("{}", x);
            // xはここで終了
        }
    
        v.push(4);  // OK(ループ後は借用が終了)
    }
    

    例3: 早期リターン

    fn example(s: &mut String, flag: bool) -> &str {
        if flag {
            return &s[..];  // 不変借用して返す
        }
    
        s.push_str(" world");  // OK(returnの後は実行されない)
        &s[..]
    }
    

    3. 借用チェックのアルゴリズム

    3.1 ローン(Loan)の概念

    借用チェッカーは、「ローン(Loan)」という概念を使います:

    let mut x = 5;
    let r = &x;  // Loan開始: x から r へ
    //           x は "貸し出し中"
    println!("{}", r);  // Loan使用
    // rの最後の使用 → Loan終了
    

    ローンの種類

    Shared Loan(共有ローン)
    - &T を作成
    - 複数同時に存在可能
    - 元の値への書き込みを禁止
    - 例: let r1 = &x; let r2 = &x;
    
    Mutable Loan(可変ローン)
    - &mut T を作成
    - 排他的(1つのみ)
    - 元の値へのアクセスを完全に禁止
    - 例: let r = &mut x;
    

    3.2 制約生成

    借用チェッカーは、コードから制約(Constraints)を生成します。

    3.2.1 制約の種類

    fn example<'a>(x: &'a i32) -> &'a i32 {
        let y = x;
        y
    }
    

    生成される制約

    // 'x: xのライフタイム
    // 'y: yのライフタイム
    // 'a: 関数のライフタイムパラメータ
    
    制約:
    1. 'y ⊇ 'x  (yはxと同じかそれ以上生きる)
    2. 'y ⊆ 'a  (yは'a以下のライフタイム)
    3. 戻り値のライフタイム = 'y
    

    3.2.2 制約の解決

    コンパイラは制約を解いて、矛盾がないか確認します:

    fn invalid<'a>(x: &'a i32) -> &'a i32 {
        let y = 42;
        &y  // エラー!yのライフタイムが短すぎる
    }
    
    // 制約:
    // 'y: yのライフタイム(関数内)
    // 'a: 関数のライフタイムパラメータ
    //
    // 矛盾: 'y < 'a だが、戻り値は 'a である必要がある
    

    3.3 到達可能性解析

    借用チェッカーは、Control Flow Graph (CFG) を使って到達可能性を解析します。

    3.3.1 CFGの構築

    fn example(condition: bool) {
        let mut x = 5;
        let r = &x;  // Loan開始
    
        if condition {
            println!("{}", r);  // 経路1: rを使用
        } else {
            // 経路2: rを使用しない
        }
    
        x = 10;  // ここでxに書き込めるか?
    }
    

    CFG

    ┌──────────────┐
    │ let r = &x   │ (Loan開始)
    └──────┬───────┘
           │
           ├─ condition = true ──┐
           │                     │
           │              ┌──────▼───────┐
           │              │ println!("{}", r) │ (rを使用)
           │              └──────┬───────┘
           │                     │
           └─ condition = false ─┤
                                 │
                          ┌──────▼───────┐
                          │ x = 10       │
                          └──────────────┘
    

    解析結果

    経路1: r が使用される → Loan が到達 → エラー
    経路2: r が使用されない → Loan は到達しない → OK
    
    結論: 経路1でコンパイルエラー
    

    エラーメッセージ

    error[E0506]: cannot assign to `x` because it is borrowed
     --> src/main.rs:9:5
      |
    3 |     let r = &x;
      |             -- borrow of `x` occurs here
    ...
    6 |         println!("{}", r);
      |                        - borrow later used here
    ...
    9 |     x = 10;
      |     ^^^^^^ assignment to borrowed `x` occurs here
    

    4. Polonius解析

    4.1 旧来の借用チェッカーの問題

    Rust 2018のNLLでも、一部のケースで不必要にエラーになることがあります:

    // Rust 2018 でもエラーになるケース
    fn get_default<'m, K, V>(
        map: &'m mut HashMap<K, V>,
        key: K,
    ) -> &'m mut V
    where
        K: Clone + Eq + Hash,
        V: Default,
    {
        match map.get_mut(&key) {
            Some(value) => value,
            None => {
                map.insert(key.clone(), V::default());
                map.get_mut(&key).unwrap()
            }
        }
    }
    // エラー!mapが借用中なのに再度借用している
    

    問題点

  • matchで借用が始まる
  • Noneブランチでも借用が続いていると見なされる
  • 実際にはNoneブランチでは借用は使われていない
  • 4.2 Poloniusの改善

    Polonius は、次世代の借用チェッカーです(現在実験的)。

    4.2.1 ローンベース vs 起源ベース

    旧来の借用チェッカー(NLL):
    - ローンベース
    - 「借用がいつ終わるか」を追跡
    - 保守的(安全側に倒す)
    
    Polonius:
    - 起源(Origin)ベース
    - 「データがどこから来たか」を追跡
    - より精密なデータフロー解析
    

    4.2.2 Poloniusの有効化

    # Cargo.toml
    [profile.dev]
    rustflags = ["-Zpolonius"]
    

    # または
    cargo +nightly rustc -- -Zpolonius
    

    4.2.3 Poloniusで解決される例

    // Poloniusでは OK
    fn example(v: &mut Vec<i32>) -> &i32 {
        if let Some(first) = v.first() {
            return first;
        }
    
        v.push(1);  // Poloniusでは OK
        &v[0]
    }
    
    // NLLではエラー:
    // error[E0502]: cannot borrow `*v` as mutable because it is also borrowed as immutable
    

    5. エラーメッセージの生成

    5.1 エラー診断の仕組み

    借用チェッカーは、エラーを検出すると、以下の情報を収集します:

    let s = String::from("hello");
    let r = &s;
    drop(s);
    println!("{}", r);
    

    収集される情報

    1. 借用の発生位置: let r = &s;
    2. 移動の発生位置: drop(s);
    3. 借用の使用位置: println!("{}", r);
    

    エラーメッセージ

    error[E0505]: cannot move out of `s` because it is borrowed
     --> src/main.rs:3:10
      |
    2 |     let r = &s;
      |             -- borrow of `s` occurs here
    3 |     drop(s);
      |          ^ move out of `s` occurs here
    4 |     println!("{}", r);
      |                    - borrow later used here
    

    5.2 修正案の自動生成

    Rustのコンパイラは、可能な限り修正案を提示します:

    let s = String::from("hello");
    drop(s);
    let r = &s;
    

    エラーメッセージ

    error[E0382]: borrow of moved value: `s`
     --> src/main.rs:3:14
      |
    1 |     let s = String::from("hello");
      |         - move occurs because `s` has type `String`,
      |           which does not implement the `Copy` trait
    2 |     drop(s);
      |          - value moved here
    3 |     let r = &s;
      |              ^^ value borrowed here after move
    
    help: consider cloning the value if the performance cost is acceptable
      |
    2 |     drop(s.clone());
      |           ++++++++
    

    5.3 エラーメッセージの改善

    Rustのエラーメッセージは、継続的に改善されています:

    改善例1: より具体的なメッセージ

    Before(古いバージョン)

    error: cannot borrow `x` as mutable
    

    After(新しいバージョン)

    error[E0502]: cannot borrow `x` as mutable because it is also borrowed as immutable
     --> src/main.rs:3:14
      |
    2 |     let r = &x;
      |             -- immutable borrow occurs here
    3 |     let r2 = &mut x;
      |              ^^^^^^ mutable borrow occurs here
    4 |     println!("{}", r);
      |                    - immutable borrow later used here
    

    改善例2: 修正案の提示

    Before

    error: cannot move out of borrowed content
    

    After

    error[E0507]: cannot move out of `*x` which is behind a shared reference
     --> src/main.rs:2:9
      |
    2 |     let s = *x;
      |         ^  ^^
      |         |  |
      |         |  move occurs because `*x` has type `String`
      |         help: consider borrowing here: `&*x`
    

    6. 借用チェッカーの内部データ構造

    6.1 ローンパス(Loan Paths)

    借用チェッカーは、ローンパスという概念を使って、どの部分が借用されているか追跡します。

    6.1.1 パスの階層

    struct Point {
        x: i32,
        y: i32,
    }
    
    let mut p = Point { x: 1, y: 2 };
    let r1 = &p.x;  // ローンパス: p.x
    let r2 = &p.y;  // ローンパス: p.y(独立)
    
    // p.x と p.y は独立したパス → 両方借用OK
    

    パスの階層

    p          (全体)
    ├─ p.x     (フィールド)
    └─ p.y     (フィールド)
    
    ルール:
    - 子パスのローンは親パスに影響
      例: &p.x がある時、 p 全体は使えない
    - 兄弟パス同士は独立
      例: &p.x と &p.y は同時に OK
    

    6.1.2 複雑なパス

    let mut v = vec![Point { x: 1, y: 2 }];
    let r = &v[0].x;  // ローンパス: v[0].x
    
    // v[0].x は借用中
    // v[0].y は借用されていない → 使える?
    // (実際にはインデックスアクセスは全体を借用する)
    

    6.2 ライフタイム推論

    6.2.1 推論アルゴリズム

    fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
        if x.len() > y.len() { x } else { y }
    }
    
    // コンパイラの推論:
    // 'a = min(x のライフタイム, y のライフタイム)
    

    推論ステップ

    1. 全てのライフタイムに変数を割り当て
       'x: xのライフタイム
       'y: yのライフタイム
       'a: 関数パラメータ
    
    2. 制約を生成
       'x ⊆ 'a
       'y ⊆ 'a
       戻り値 ⊆ 'a
    
    3. 制約を解く(最小不動点アルゴリズム)
       'a = min('x, 'y)
    
    4. 矛盾があればエラー
    

    7. 高度な借用パターン

    7.1 分割借用(Split Borrowing)

    let mut arr = [1, 2, 3, 4, 5];
    let (left, right) = arr.split_at_mut(2);
    // left と right は独立した可変参照
    left[0] = 10;
    right[0] = 20;
    

    内部実装

    pub fn split_at_mut(&mut self, mid: usize) -> (&mut [T], &mut [T]) {
        let len = self.len();
        let ptr = self.as_mut_ptr();
    
        unsafe {
            (
                slice::from_raw_parts_mut(ptr, mid),
                slice::from_raw_parts_mut(ptr.add(mid), len - mid),
            )
        }
    }
    // unsafe を使ってメモリが重複しないことを保証
    

    7.2 リバッファリング(Reborrowing)

    fn foo(s: &mut String) {
        bar(&mut *s);  // リバッファ
        //       ^^
        //       明示的なデリファレンス
    
        // sはまだ使える
        s.push_str("!");
    }
    
    fn bar(s: &mut String) {
        s.push_str("bar");
    }
    

    仕組み

    1. &mut *s で一時的な可変参照を作る
    2. bar() に渡す
    3. bar() が終わると、元のsが使える
    

    まとめ

    借用チェッカーの進化

    Rust 1.0 (2015):
    - Lexical Lifetimes
    - 保守的だが単純
    
    Rust 2018:
    - Non-Lexical Lifetimes
    - より賢いデータフロー解析
    
    Future (Polonius):
    - Origin-based analysis
    - さらに精密
    

    重要なポイント

  • 借用チェッカーはMIRレベルで動作
  • データフロー解析でローンを追跡
  • NLLで実際の使用期間を追跡
  • エラーメッセージは非常に親切
  • 継続的に改善されている
  • 実用的なヒント

  • エラーメッセージを読む: 修正案が含まれている
  • MIRを確認する: --emit=mir で内部表現を見る
  • Poloniusを試す: 実験的だが有用
  • パターンを学ぶ: 分割借用、リバッファリング

次の章へ

次の章では、ライフタイム基礎を学びます。ライフタイム注釈の書き方、ライフタイムエリジョン、サブタイプ関係など、より実践的な内容に進みます。