クロージャキャプチャ推論
このセクションでは、rustc がクロージャをどのように扱うかを説明します。Rust のクロージャは、作成元のスタックフレームから、使用する値(または使用する値への参照)を含む構造体へ実質的に「脱糖」されます。rustc の仕事は、クロージャがどの値をどのように使用するかを把握し、特定の変数を共有参照、可変参照、またはムーブのどれでキャプチャするかを決定できるようにすることです。rustc はまた、クロージャがどのクロージャトレイト(Fn、FnMut、または FnOnce)を実装できるかも把握する必要があります。
いくつかの例から始めましょう。
例 1
まず、次の例のクロージャがどのように脱糖されるかを見てみましょう。
fn closure(f: impl Fn()) {
f();
}
fn main() {
let x: i32 = 10;
closure(|| println!("Hi {}", x)); // クロージャは x を読み取るだけです。
println!("Value of x after return {}", x);
}
上記が immut.rs というファイルの内容だとします。次のコマンドを使用して
immut.rs をコンパイルします。-Z dump-mir=all フラグにより、
rustc は MIR を生成し、mir_dump というディレクトリにダンプします。
> rustc +stage1 immut.rs -Z dump-mir=all
このコマンドを実行すると、現在の作業ディレクトリに mir_dump という新しく生成されたディレクトリがあることがわかります。このディレクトリには複数のファイルが含まれています。
ファイル rustc.main.-------.mir_map.0.mir を見ると、ほかの内容に加えて、次の行も含まれていることがわかります。
_4 = &_1;
_3 = [closure@immut.rs:7:13: 7:36] { x: move _4 };
この章の MIR の例では、_1 が x であることに注意してください。
ここで、1 行目 _4 = &_1; において、mir_dump は x が不変参照として借用されたことを示しています。これは、クロージャが x を読み取るだけであるため、期待どおりです。
例 2
別の例を示します。
fn closure(mut f: impl FnMut()) {
f();
}
fn main() {
let mut x: i32 = 10;
closure(|| {
x += 10; // クロージャは x の値を変更します
println!("Hi {}", x)
});
println!("Value of x after return {}", x);
}
_4 = &mut _1;
_3 = [closure@mut.rs:7:13: 10:6] { x: move _4 };
今回は、_4 = &mut _1; という行で、借用が可変借用に変更されていることがわかります。
妥当です!クロージャは x に 10 を加算しています。
例 3
もう 1 つ例を示します。
fn closure(f: impl FnOnce()) {
f();
}
fn main() {
let x = vec![21];
closure(|| {
drop(x); // 以後 x を使用できなくします。
});
// println!("Value of x after return {:?}", x);
}
_6 = [closure@move.rs:7:13: 9:6] { x: move _1 }; // bb16[3]: move.rs:7:13: 9:6 のスコープ 1
ここでは、x はクロージャに直接ムーブされ、クロージャの後にそれへアクセスすることは許可されません。
コンパイラにおける推論
それでは rustc のコードを詳しく見て、これらすべての推論がコンパイラによってどのように行われるかを確認しましょう。
まず、この後の議論で頻繁に使用する用語を定義することから始めましょう -
upvar です。upvar とは、クロージャが定義されている関数にローカルな変数です。したがって、上記の例では、x はクロージャに対する upvar になります。これらは 自由変数 と呼ばれることもあります。これは、クロージャのコンテキストに束縛されていないことを意味します。
compiler/rustc_passes/src/upvars.rs は、この目的のために upvars_mentioned というクエリを定義しています。
遅延呼び出し以外で、クロージャを通常の関数と区別するもう 1 つの点は、upvar を使用できることです。クロージャは周囲のコンテキストからこれらの upvar を借用します。そのため、コンパイラは upvar の借用型を決定する必要があります。コンパイラは不変借用型を割り当てることから始め、使用方法に基づいて必要に応じて制約を弱めます(つまり、immutable から mutable、さらに move へ変更します)。上記の例 1 では、クロージャは変数を出力のために使用するだけで、いかなる方法でも変更しません。そのため、mir_dump では upvar x の借用型が不変であることがわかります。しかし例 2 では、クロージャは x を変更し、何らかの値を加算します。この変更のために、最初は x を不変参照型として割り当てていたコンパイラは、それを可変参照として調整する必要があります。同様に 3 番目の例では、クロージャがベクターをドロップするため、変数 x をクロージャへムーブする必要があります。借用の種類に応じて、クロージャは適切なトレイトを実装する必要があります。不変借用では Fn トレイト、可変借用では FnMut、ムーブセマンティクスでは FnOnce です。
クロージャに関連するコードの大部分は
compiler/rustc_hir_typeck/src/upvar.rs ファイルにあり、データ構造は
compiler/rustc_middle/src/ty/mod.rs ファイルで宣言されています。
先に進む前に、rustc コードベースを通る制御の流れをどのように調べられるかを説明しましょう。特にクロージャについては、次のように RUSTC_LOG 環境変数を設定し、出力をファイルに収集します。
> RUSTC_LOG=rustc_hir_typeck::upvar rustc +stage1 -Z dump-mir=all \
<.rs file to compile> 2> <file where the output will be dumped>
これは stage1 コンパイラを使用し、
rustc_hir_typeck::upvar モジュールの debug! ロギングを有効にします。
もう 1 つの選択肢は、lldb または gdb を使用してコードをステップ実行することです。
rust-lldb build/host/stage1/bin/rustc test.rs- lldb で:
b upvar.rs:134// upvar.rs ファイル内の特定の行にブレークポイントを設定するr// ブレークポイントに到達するまでプログラムを実行する
upvar.rs から始めましょう。このファイルには
euv::ExprUseVisitor と呼ばれるものがあり、クロージャのソースを走査し、
借用、変更、またはムーブされる各 upvar に対してコールバックを呼び出します。
fn main() {
let mut x = vec![21];
let _cl = || {
let y = x[0]; // 1.
x[0] += 1; // 2.
};
}
上記の例では、ビジターは 1 と 2 で示された行に対して 2 回呼び出されます。1 回は共有借用に対して、もう 1 回は可変借用に対してです。また、何が借用されたかも教えてくれます。
コールバックは Delegate トレイトを実装することで定義されます。
InferBorrowKind 型は Delegate を実装し、
各 upvar についてどのキャプチャモードが必要だったかを記録するマップを保持します。キャプチャモードには ByValue(ムーブされた)または ByRef(借用された)があります。ByRef 借用の場合、可能な
[BorrowKind] は [compiler/rustc_middle/src/ty/mod.rs][middle_ty] で定義されている ImmBorrow、UniqueImmBorrow、MutBorrow です。
[BorrowKind]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/enum.BorrowKind.html
[middle_ty]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/index.html
Delegate は、いくつかの異なるメソッド(異なるコールバック)を定義します。
変数の move には consume、何らかの種類の borrow
(共有または可変)には borrow、何かの assignment が見つかったときには
mutate です。
これらのコールバックにはすべて共通の引数 cmt があります。これは Category、
Mutability、Type を表し、
compiler/rustc_hir_typeck/src/expr_use_visitor.rs で定義されています。コード
コメントから引用すると、「cmt は、値がどこから発生し、どのように配置されているか、
およびその値が格納されているメモリの可変性を示す、値の完全な分類です」。コールバック
(consume、borrow など)に基づいて、関連する adjust_upvar_borrow_kind_for_<something> を呼び出し、
cmt を渡します。borrow 型が調整されると、それをテーブルに格納します。これは基本的に、
各クロージャに対してどの borrow が行われたかを示します。
self.tables
.borrow_mut()
.upvar_capture_map
.extend(delegate.adjust_upvar_captures);