型付け/パラメーター環境
型付け環境
型システムとやり取りする際には、トレイト解決の結果に影響し得るいくつかの変数を考慮する必要があります。
スコープ内の where 句の集合と、コンパイラーの型システム操作がどのフェーズで実行されているか(それぞれ [ParamEnv][penv] 構造体と [TypingMode][tmode] 構造体)です。
型システム操作を実行するための環境がまだ作成されていない場合、
[TypingEnv][tenv] を使用して、必要なすべての外部コンテキストを単一の型にまとめることができます。
型システム操作を実行するためのコンテキストが作成されると(例: ObligationCtxt や FnCtxt)、通常 TypingEnv はどこにも保存されません。これは TypingMode だけが環境全体のプロパティである一方で、
異なる ParamEnv はゴールごとに使用できるためです。
パラメーター環境
ParamEnv とは何か
[ParamEnv][penv] は、スコープ内の where 句のリストです。
これは通常、特定のアイテムの where 句に対応します。
一部の句は明示的には書かれず、代わりに predicates_of クエリで暗黙的に追加されます。
たとえば ConstArgHasType や(一部の)implied bounds です。
ほとんどの場合、ParamEnv は最初に param_env クエリを介して作成されます。このクエリは、指定されたアイテムの where 句から派生した ParamEnv を返します。
ParamEnv は、特定のアイテムから派生したものではない任意の句の集合で作成することもできます。
たとえば compare_method_predicate_entailment では、impl の where 句とトレイト定義の関数の where 句からなるハイブリッドな ParamEnv を作成しています。
次のような関数がある場合:
#![allow(unused)]
fn main() {
// `foo` は次の `ParamEnv` を持つことになります:
// `[T: Sized, T: Trait, <T as Trait>::Assoc: Clone]`
fn foo<T: Trait>()
where
<T as Trait>::Assoc: Clone,
{}
}
概念的に foo の内部にいる場合(たとえば、型チェックや lint を行っている場合)、型システムとやり取りするすべての箇所でこの ParamEnv を使用します。
これにより、正規化、ジェネリック定数の評価、
where 句/ゴールの証明などが、T が sized であり、Trait を実装している、などの事実に依存できるようになります。
より具体的な例:
#![allow(unused)]
fn main() {
// `foo` は次の `ParamEnv` を持つことになります:
// `[T: Sized, T: Clone]`
fn foo<T: Clone>(a: T) {
// `foo` を型チェックするとき、`requires_clone` 上のすべての where 句が
// 呼び出しを合法にするために成り立つことを要求します。これは、
// `T: Clone` を証明しなければならないことを意味します。`foo` を型チェックしているため、
// `T: Clone` が成り立つことを確認しようとするときには `foo` の
// 環境を使用します。
//
// `[T: Sized, T: Clone]` の `ParamEnv` で `T: Clone` を証明しようとすると、
// 証明したい境界が環境内にあるため自明に成功します。
requires_clone(a);
}
}
あるいは、コンパイルされない例:
#![allow(unused)]
fn main() {
// `foo2` は次の `ParamEnv` を持つことになります:
// `[T: Sized]`
fn foo2<T>(a: T) {
// `foo2` を型チェックするとき、`T: Clone` を証明しようとします。
// `foo2` を型チェックしているため、`T: Clone` を証明しようとするときには
// `foo2` の環境を使用します。
//
// `[T: Sized]` の `ParamEnv` で `T: Clone` を証明しようとすると、
// トレイトソルバーに `T` が `Clone` を実装していることを伝えるものが
// 環境内に何もなく、適用可能なユーザー記述の impl も存在しないため、
// 失敗します。
requires_clone(a);
}
}
ParamEnv の取得
型システムとやり取りする際に誤った [ParamEnv][penv] を使用すると、ICE、
不正形式のプログラムのコンパイル成功、またはエラーにすべきでない箇所でのエラーにつながる可能性があります。
コンパイラーを正しい param env を使用するように変更し、その過程で ICE を修正した PR の例として、#82159 と #82067 を参照してください。
大多数の場合、ParamEnv が必要なときには、それはすでにスコープ内のどこかに存在しているか、
呼び出しスタックの上位にあり、下位へ渡されるべきものです。
既存の ParamEnv が見つかる可能性がある場所の非網羅的なリスト:
- typeck 中は、
FnCtxtに [param_envフィールド][fnctxt_param_env]があります - late lint を書くときは、
LateContextに [param_envフィールド][latectxt_param_env]があります - well-formedness チェック中は、
WfCheckingCtxtに [param_envフィールド][wfckctxt_param_env]があります - MIR Typeck に使用される
TypeCheckerには [param_envフィールド][mirtypeck_param_env]があります - 次世代トレイトソルバーでは、すべての
Goalに、そのゴールをどの環境で証明するかを指定する [param_envフィールド][goal_param_env]があります - 既存の [
TypeRelation][typerelation] を編集していて、それが [PredicateEmittingRelation][predicate_emitting_relation] を実装している場合、[param_envメソッド][typerelation_param_env]が利用可能です。
使用できる ParamEnv がスコープ内のどこかにあるかどうかわからない場合は、[#t-compiler/help][compiler_help] Zulip チャンネルでスレッドを開く価値があります。そこでは、誰かが ParamEnv をどこから取得できるかを指摘してくれるかもしれません。
手動で ParamEnv を構築する必要があるのは、通常、何らかのトップレベル解析(例: hir typeck や borrow checking)の開始時だけです。
そのような場合、これを行う方法は 3 つあります:
- [
tcx.param_env(def_id)クエリ][param_env_query]を呼び出す。これは、指定された定義に関連付けられた環境を返します。 - [
ParamEnv::empty][env_empty] で空の環境を作成する。 - [
ParamEnv::new][param_env_new] を使用して、任意の where 句の集合を持つ env を構築する。 その後、traits::normalize_param_env_or_errorを呼び出して、env 内のすべての where 句の正規化と elaboration を処理する。
ほとんどの場合、コンパイラーは特定の定義の一部として解析を実行しているため、ParamEnv を構築する最も一般的な方法は、圧倒的に param_env クエリを使用することです。
ParamEnv::empty で空の環境を作成するのは、通常、コード生成([TypingEnv::fully_monomorphized][tenv_mono] を介して間接的に)で行う場合か、
ジェネリックパラメーターに遭遇することが決してないと想定している解析の一部として行う場合だけです
(例: coherence/orphan チェックのさまざまな部分)。
任意の where 句の集合から env を作成することは通常不要であり、必要な環境がソースコード内の実際のアイテムに対応していない場合にのみ行うべきです(例: compare_method_predicate_entailment)。
[param_env_new]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/struct.ParamEnv.html#method.new
normalize_env_or_error: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_trait_selection/traits/fn.normalize_param_env_or_error.html
[fnctxt_param_env]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir_typeck/fn_ctxt/struct.FnCtxt.html#structfield.param_env
[latectxt_param_env]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_lint/context/struct.LateContext.html#structfield.param_env
[wfckctxt_param_env]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir_analysis/check/wfcheck/struct.WfCheckingCtxt.html#structfield.param_env
[goal_param_env]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_infer/infer/canonical/ir/solve/struct.Goal.html#structfield.param_env
[typerelation_param_env]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_infer/infer/trait.PredicateEmittingRelation.html#tymethod.param_env
[typerelation]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/relate/trait.TypeRelation.html
[mirtypeck_param_env]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_borrowck/type_check/struct.TypeChecker.html#structfield.param_env
[env_empty]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/struct.ParamEnv.html#method.empty
[param_env_query]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir_typeck/fn_ctxt/struct.FnCtxt.html#structfield.param_env
method_pred_entailment: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir_analysis/check/compare_impl_item/fn.compare_method_predicate_entailment.html
[predicate_emitting_relation]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/relate/combine/trait.PredicateEmittingRelation.html
[tenv_mono]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/struct.TypingEnv.html#method.fully_monomorphized
[compiler_help]: https://rust-lang.zulipchat.com/#narrow/channel/182449-t-compiler.2Fhelp
ParamEnv はどのように構築されるか
ParamEnv の作成は、ユーザーが記述したとおりにアイテム上で定義されたwhere句のリストを単純に使用するよりも複雑です。
スーパーtraitを環境へ展開し、すべてのエイリアスを完全に正規化する必要があります。
このロジックは traits::normalize_param_env_or_error によって処理されます(ただし、その名前には展開について何も言及されていません)。
スーパーtraitの展開
fn foo<T: Copy>() のような関数がある場合、Copy traitには Clone スーパーtraitがあるため、関数内で T: Clone を証明できるようにしたいと考えます。
ParamEnv を構築する際には、環境内のすべてのtrait境界を見て、それらのtrait上に見つかったスーパーtraitについて、新しいwhere句を ParamEnv に明示的に追加します。
具体的な例は次の関数です。
#![allow(unused)]
fn main() {
trait Trait: SuperTrait {}
trait SuperTrait: SuperSuperTrait {}
// `bar` の展開前の `ParamEnv` は次のようになります:
// `[T: Sized, T: Copy, T: Trait]`
fn bar<T: Copy + Trait>(a: T) {
requires_impl(a);
}
fn requires_impl<T: Clone + SuperSuperTrait>(a: T) {}
}
環境を展開しなければ、T: Clone や T: SuperSuperTrait を証明できないため、requires_impl 呼び出しは型チェックに失敗します。
実際には環境を展開するため、bar の ParamEnv は実際には次のようになります。
[T: Sized, T: Copy, T: Clone, T: Trait, T: SuperTrait, T: SuperSuperTrait]
これにより、bar の型チェック時に T: Clone と T: SuperSuperTrait を証明できます。
Clone traitには Sized スーパーtraitがありますが、環境内に2つの T: Sized 境界(1つはスーパーtrait由来、もう1つは暗黙に追加される T: Sized 境界由来)が存在することにはなりません。これは、展開プロセス(util::elaborate によって実装)がwhere句を重複排除するためです。
この副作用として、スーパーtraitの実際の展開が行われない場合でも、 環境内の既存のwhere句も重複排除されます。 次の例を参照してください。
#![allow(unused)]
fn main() {
trait Trait {}
// 展開前の `ParamEnv` は次のようになります:
// `[T: Sized, T: Trait, T: Trait]`
// しかし展開後は次のようになります:
// `[T: Sized, T: Trait]`
fn foo<T: Trait + Trait>() {}
}
次世代traitソルバー でも、この展開が行われる必要があります。
すべての境界の正規化
古いtraitソルバーでは、ParamEnv に格納されるwhere句は完全に正規化されている必要があります。そうでない場合、traitソルバーは正しく機能しません。
ParamEnv の正規化が必要になる具体例は次のとおりです。
#![allow(unused)]
fn main() {
trait Trait<T> {
type Assoc;
}
trait Other {
type Bar;
}
impl<T> Other for T {
type Bar = u32;
}
// `foo` の正規化前の `ParamEnv` は次のようになります:
// `[T: Sized, U: Sized, U: Trait<T::Bar>]`
fn foo<T, U>(a: U)
where
U: Trait<<T as Other>::Bar>,
{
requires_impl(a);
}
fn requires_impl<U: Trait<u32>>(_: U) {}
}
人間には、<T as Other>::Bar が u32 と等しいことが分かるため、U 上のtrait境界は U: Trait<u32> と同等です。
実際には、この環境で古いソルバーを使って U: Trait<u32> を証明しようとすると、<T as Other>::Bar が u32 と等しいことを判断できないため失敗します。
これを回避するため、ParamEnv を構築した後に正規化します。そのため、foo の ParamEnv は実際には [T: Sized, U: Sized, U: Trait<u32>] となり、traitソルバーは ParamEnv 内の U: Trait<u32> を使用して、trait境界 U: Trait<u32> が成立することを判断できるようになります。
この回避策はすべての場合に機能するわけではありません。関連型の正規化には ParamEnv が必要であり、それがブートストラップ問題を引き起こすためです。
正規化が正しい結果を返すには正規化済みの ParamEnv が必要ですが、その ParamEnv を得るには正規化が必要です。
現在は、正規化前のparam envを使用して ParamEnv を一度だけ正規化しており、これが壊れる例はいくつかあるものの(example)、実際にはおおむね問題ない結果を返す傾向があります。
次世代traitソルバーでは、ParamEnv 内のすべてのwhere句が完全に正規化されている必要はないため、ParamEnv を構築する際に正規化は行いません。
型付けモード
型システム操作をどのコンテキストで実行しているかに応じて、 必要となる振る舞いが異なる場合があります。 たとえばコヒーレンス中には、ゴールが成立しないと見なせるタイミングや、型が等しくないと見なせるタイミングについて、より強い要件があります。
コンパイラの型システム操作がどの「フェーズ」で実行されているかの追跡は、[TypingMode][tmode] enumによって行われます。
TypingMode enumに関するドキュメントはかなり優れているため、ここでそのまま繰り返す代わりに、APIドキュメントを直接読むことをおすすめします。
[penv]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/struct.ParamEnv.html
[tenv]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/struct.TypingEnv.html
[tmode]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/type.TypingMode.html