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の利点:
&mut _11.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
- さらに精密
重要なポイント
実用的なヒント
--emit=mir で内部表現を見る次の章へ
次の章では、ライフタイム基礎を学びます。ライフタイム注釈の書き方、ライフタイムエリジョン、サブタイプ関係など、より実践的な内容に進みます。