Binder のインスタンス化
EarlyBinder とよく似て、Binder の内側にアクセスするときは、まず束縛された変数を何らかの別の値で置き換えることで、それを解放しなければなりません。
これは EarlyBinder の場合とほぼ同じ理由によるもので、Binder によって導入されたパラメータを参照する型は、その binder の外側では意味をなしません。
次のエラーになる例を見てください。
fn foo<'a>(a: &'a u32) -> &'a u32 {
a
}
fn bar<T>(a: fn(&u32) -> T) -> T {
a(&10)
}
fn main() {
let higher_ranked_fn_ptr = foo as for<'a> fn(&'a u32) -> &'a u32;
// `T=for<'a> &'a u32` と推論しようとするが、これは満たせない
let references_bound_vars = bar(higher_ranked_fn_ptr);
}
この例では、型 for<'a> fn(&'^0 u32) -> &'^0 u32 の引数を bar に渡しています。
T が型 &'^0 u32 に推論されることは許可したくありません。これはかなり意味をなさないためです(たまたま ICE しなかった場合、おそらく unsound でもあります)。
main は 'a について知らないため、borrow checker はライフタイム 'a を持つ borrow を扱うことができません。
EarlyBinder とは異なり、通常 Binder をユーザーからの何らかの具体的な引数集合でインスタンス化することはありません。つまり、for<'a1, 'a2> fn(&'a1 u32, &'a2 u32) に対する引数として ['b, 'static] を使うようなことは通常しません。代わりに、通常は binder を推論変数または placeholder でインスタンス化します。
推論変数によるインスタンス化
binder の可能なインスタンス化を推論しようとしているとき、たとえば higher-ranked 関数ポインタを呼び出す場合や、higher-ranked where-clause を使って何らかの bound を証明しようとする場合に、binder を推論変数でインスタンス化します。たとえば、上の例の higher_ranked_fn_ptr が与えられ、それを &10_u32 で呼び出す場合は、次のようになります。
- binder を推論変数でインスタンス化し、
fn(&'?0 u32) -> &'?0 u32)というシグネチャを得る - 渡された引数
&10_u32(&’static u32)の型を、シグネチャ内の型&'?0 u32と等置し、'?0 = 'staticと推論する - 渡された引数の型を fn ptr シグネチャ内の引数の型と正常に unify できたため、渡された引数は正しかった
推論変数によるインスタンス化の別の例として、何らかの for<'a> T: Trait<'a> where-clause が与えられ、T: Trait<'static> が成り立つことを証明しようとしている場合は、次のようになります。
- binder を推論変数でインスタンス化し、
T: Trait<'?0>という where clause を得る T: Trait<'static>という goal を、インスタンス化された where clause と等置し、'?0 = 'staticと推論するT: Trait<'static>をT: Trait<'?0>と正常に unify できたため、goal は成り立つ
binder を推論変数でインスタンス化するには、InferCtxt の instantiate_binder_with_fresh_vars メソッドを使用します。
binder の特定の 1 つのインスタンス化だけを考えればよい場合は、推論変数で binder をインスタンス化するべきです。一方で、binder のすべての可能なインスタンス化について推論したい場合は、代わりに placeholder を使用するべきです。
placeholder によるインスタンス化
placeholder は Ty/ConstKind::Param/ReEarlyParam と非常によく似ており、それ自体とだけ等しい何らかの未知の型を表します。
Ty/Const と Region はすべて、Universe と BoundVar から構成される Placeholder variant を持ちます。
Universe は placeholder がどの binder に由来するかを追跡し、BoundVar はこの placeholder がその binder 上のどのパラメータに対応するかを追跡します。
placeholder の等価性は、universe が等しく、かつ BoundVar が等しいかどうかのみによって決まります。
詳細については、Placeholders and Universes の章を参照してください。
他の rustc 開発者と話すときや、Debug フォーマットされた Ty/Const/Region を見るとき、Placeholder はしばしば '!UNIVERSE_BOUNDVARS と書かれます。
たとえば、ある型 for<'a> fn(&'a u32, for<'b> fn(&'b &'a u32)) が与えられた場合、
両方の binder をインスタンス化した後(事前に現在の InferCtxt 内の Universe が U0 だったと仮定します)、
&'b &'a u32 の型は &'!2_0 &!1_0 u32 と表現されます。
placeholder の universe が 0 の場合、debug 出力では完全に省略されます。つまり、!0_2 は !2 として出力されます。
ただし、placeholder で binder をインスタンス化するときには InferCtxt 内の universe を増やすため、実際にはこれはまれにしか起こりません。
したがって、通常遭遇しうる最小の universe の placeholder は U1 内のものです。
Binder は、InferCtxt の enter_forall メソッドを介して placeholder でインスタンス化できます。
コンパイラが、binder の 1 つの具体的なインスタンス化ではなく、あらゆる可能なインスタンス化を考慮するべき場合に使用するべきです。
注: この章の元の例では、ローカル変数が型 &'^0 u32 を持つと推論するべきではないと述べました。
このコードは universes によってコンパイルが防止されます(リンク先の章で説明しているとおりです)。
なぜ RePlaceholder と ReBound の両方があるのか?
なぜこれら両方の variant があるのか疑問に思うかもしれません。結局のところ、Placeholder に格納されるデータは実質的に ReBound のデータと同等です。つまり、どの binder かを追跡するものと、Binder が導入したどのパラメータかを追跡する index です。
この主な理由は、Bound が束縛変数のより構文的な表現であるのに対し、Placeholder はより意味的な表現であることです。
具体例として、次を見てください。
#![allow(unused)]
fn main() {
impl<'a> Other<'a> for &'a u32 { }
impl<T> Trait for T
where
for<'a> T: Other<'a>,
{ ... }
impl<T> Bar for T
where
for<'a> &'a T: Trait
{ ... }
}
これらの trait implementation が与えられた場合、u32: Bar は成り立つべきでは_ありません_。
&'a u32 が Other<'a> を実装するのは、borrow のライフタイムと trait 上のライフタイムが等しい場合だけです。
しかし、ReBound だけを使用し、placeholder がなかった場合、その trait bound が成り立つと誤って信じてしまいやすいかもしれません。
これを説明するために、rustc に placeholder がない世界で u32: Bar を証明しようとする例を追ってみましょう。
- まず
u32: Barを証明しようとする impl<T> Bar for Timpl を見つけ、EarlyBinderをu32でインスタンス化することになる(注: 実際には、まず binder を推論変数でインスタンス化し、その後それがu32であると推論するため、これは_完全に_正確ではありませんが、ここではその違いはそれほど重要ではありません)- impl 上には where clause
for<'a> &'^0 T: Traitがあり、early binder をu32でインスタンス化したため、実際にはfor<'a> &'^0 u32: Traitを証明する必要がある impl<T> Trait for Timpl を見つけ、EarlyBinderを&'^0 u32でインスタンス化することになる- where clause
for<'a> T: Other<'^0>があり、early binder を&'^0 u32でインスタンス化したため、実際にはfor<'a> &'^0 u32: Other<'^0>を証明する必要がある impl<'a> Other<'a> for &'a u32を見つけ、この impl は bound を証明するのに十分である。なぜなら borrow 上のライフタイムと trait 上のライフタイムがどちらも'^0だからである
この最終結果は正しくありません。というのも、独自のジェネリックパラメータを導入する 2 つの別々の binder があったため、trait bound は for<'a1, 'a2> &'^1 u32: Other<'^0> のようなものになっているべきであり、これは impl<'a> Other<'a> for &'a u32 によって満たされ_ない_からです。
理論上はこれを動作させることもできますが、かなり込み入っており、現在の構成よりも複雑になります。次のことを行う必要があるでしょう。
Boundの ty/const/region でBinder/EarlyBinderをインスタンス化するたびに、束縛変数のDebruijnIndexがより高くなるように「書き換える」- 推論変数を束縛変数に推論する際、その束縛変数が、推論変数の作成後に入った binder に由来する場合は、その変数の
DebruijnIndexを下げる必要がある。 - 推論変数がどの binder の内側で作成されたかを別途追跡し、さらにその推論変数がどの最内の binder からパラメーターを名前指定できるかも追跡する(現在は後者だけを追跡すればよい)
- 推論変数を解決する際、infcx の現在の binder 深さに応じて、あらゆる束縛変数を書き換える
- ほかにもあるかもしれない(このリストを書いている間にも項目が増え続けたので、これが網羅的だと考えるのは単純すぎるように思える)
根本的に、この複雑さはすべて、Bound の ty/const/region では、Binder 上のあるパラメーターに対する表現が、そのパラメーターを導入する binder と、その使用箇所の間に存在する他の Binder の数によって異なるために生じます。
たとえば、次のコードがあるとします。
#![allow(unused)]
fn main() {
fn foo<T>()
where
for<'a> T: Trait<'a, for<'b> fn(&'b T, &'a u32)>
{ ... }
}
この where 句は for<'a> T: Trait<'^0, for<'b> fn(&'^0 T, &'^1_0 u32)> と書かれます。
'a パラメーターへの参照が 2 つあるにもかかわらず、
それらは ^0 と ^1_0 というように異なる形で表現されます。
これは、後者の使用箇所が、内側の関数ポインター型のための 2 つ目の Binder の下にネストされているためです。
これは Placeholder の ty/const/region とは対照的です。Placeholder にはこの制限がありません。なぜなら、Universe はパラメーターの使用箇所ではなく、現在の InferCtxt に固有のものだからです。
EarlyBinder をインスタンス化したり、推論変数を既存の Placeholder と単一化したりすることは自明に可能です。Placeholder がどのコンテキストにあっても、同じ表現を持つためです。
例として、上記の高ランク where 句上の binder をインスタンス化した場合、次のように表現されます。
T: Trait<'!1_0, for<'b> fn(&'^0 T, &'!1_0 u32)>.
'a の両方の使用箇所に対する RePlaceholder の表現は、一方が別の Binder の下にあるにもかかわらず同じです。
その後、関数ポインター上の binder をインスタンス化すると、次のような型が得られます。
fn(&'!2_0 T, ^'!1_0 u32)
'b パラメーターの RePlaceholder は、'a の binder の後にその binder がインスタンス化されたという事実を追跡するため、より高い universe にあります。
ReLateParam によるインスタンス化
型の表現に関する章で説明したように、RegionKind にはジェネリックパラメーターを表現するための 2 つのバリアント、ReLateParam と ReEarlyParam があります。
ReLateParam は概念的には、常にルート universe(U0)に存在する Placeholder です。
これは、関数/クロージャの内部にいる間に、それらの遅延束縛パラメーターをインスタンス化する際に使用されます。
その実際の表現は、ReEarlyParam と RePlaceholder のどちらとも比較的異なっています。
- 遅延束縛ジェネリックパラメーターを導入したアイテムの
DefId - ジェネリックパラメーターの
DefIdとその名前(Symbol経由)を指定するか、このプレースホルダーがFn/FnMutクロージャの self borrow の匿名ライフタイムを表していることを示すBoundRegionKind。BrAnonのためのバリアントもありますが、これはReLateParamには使用されません。
たとえば、次のコードがあるとします。
impl Trait for Whatever {
fn foo<'a>(a: &'a u32) -> &'a u32 {
let b: &'a u32 = a;
b
}
}
関数本体内の型 &'a u32 におけるライフタイム 'a は、次のように表現されます。
ReLateParam(
{impl#0}::foo,
BoundRegionKind::BrNamed({impl#0}::foo::'a, "'a")
)
関数の本体内からその関数の遅延束縛ジェネリックパラメーターを参照するこの特定のケースでは、
これはどこかで Binder をインスタンス化する際に明示的に行われるのではなく、
hir_ty_lowering の間に暗黙的に行われます。
ただし場合によっては、Binder を ReLateParam で明示的にインスタンス化します。
一般に、関数/クロージャ上の遅延束縛パラメーターに対する Binder があり、
概念的にはすでにその binder の内側にいる場合、
liberate_late_bound_regions を使用して、それを ReLateParam でインスタンス化します。
これにより、この操作は EarlyBinder の instantiate_identity に相当する Binder の操作になります。
具体例として、型チェック中の関数のシグネチャにアクセスすると、それは EarlyBinder<Binder<FnSig>> として表現されます。
すでにこれらの binder の「内側」にいるため、instantiate_identity を呼び出した後に liberate_late_bound_regions を呼び出します。