ty モジュール: 型の表現
ty モジュールは、Rust コンパイラが内部で型をどのように表現するかを定義します。
また、コンパイラにおける中心的なデータ構造である
型付けコンテキスト(tcx または TyCtxt)も定義します。
ty::Ty
rustc が型をどのように表現するかについて話すとき、通常は Ty と呼ばれる型を指します。
コンパイラには Ty に関するモジュールや型がかなり多くあります(Ty のドキュメント)。
ここで指している具体的な Ty は rustc_middle::ty::Ty です
(rustc_hir::Ty ではありません)。
この区別は重要なので、ty::Ty の詳細に入る前に、まずそれについて説明します。
rustc_hir::Ty と ty::Ty
rustc における HIR は、高レベル中間表現と考えることができます。 これは多かれ少なかれ AST(この章を参照)であり、ユーザーが書いた構文を表現し、構文解析といくつかの脱糖化の後に得られます。 HIR には型の表現がありますが、実際にはユーザーが書いたもの、つまりその型を表現するために書いたものをより強く反映しています。
対照的に、ty::Ty は型のセマンティクス、つまりユーザーが書いたものの意味を表します。
たとえば、rustc_hir::Ty はユーザーがプログラム内で u32 という名前を 2 回使ったという事実を記録しますが、
ty::Ty はその両方の使用が同じ型を指しているという事実を記録します。
例: fn foo(x: u32) → u32 { x }
この関数では、u32 が 2 回現れていることがわかります。
それが同じ型であること、つまりこの関数がある型の引数を受け取り、同じ型の引数を返すことはわかっています。
しかし HIR の観点からは、これらはプログラム内の 2 つの異なる場所に現れているため、2 つの別個の型インスタンスになります。
つまり、それぞれ異なる Span(位置)を持っています。
例: fn foo(x: &u32) -> &u32
さらに、HIR では情報が省かれている場合があります。
この型 &u32 は不完全です。完全な Rust の型には実際にはライフタイムが存在しますが、そのライフタイムを書く必要がなかったためです。
情報を挿入する省略規則もいくつかあります。
その結果は fn foo<'a>(x: &'a u32) -> &'a u32 のようになるかもしれません。
HIR レベルでは、これらは明示されておらず、かなり不完全な図になっていると言えます。
しかし、ty::Ty レベルでは、これらの詳細が追加され、完全になります。
さらに、u32 のような特定の型に対しては正確に 1 つの ty::Ty が存在し、その ty::Ty は rustc_hir::Ty とは異なり、特定の使用箇所ではなく、プログラム全体のすべての u32 に対して使用されます。
まとめると次のようになります。
rustc_hir::Ty | ty::Ty |
|---|---|
| 型の構文、つまりユーザーが書いたもの(いくつかの脱糖化を含む)を記述する。 | 型のセマンティクス、つまりユーザーが書いたものの意味を記述する。 |
各 rustc_hir::Ty は、プログラム内の該当する場所に対応する独自の span を持つ。 | ユーザーのプログラム内の単一の場所には対応しない。 |
rustc_hir::Ty にはジェネリクスとライフタイムがある。ただし、それらのライフタイムの一部は LifetimeKind::Implicit のような特別なマーカーである。 | ty::Ty には、ユーザーが省いていたとしても、ジェネリクスとライフタイムを含む完全な型がある。 |
fn foo(x: u32) -> u32 { } - u32 の各使用を表す 2 つの rustc_hir::Ty があり、それぞれが独自の Span を持つ。また、rustc_hir::Ty は両方が同じ型であることを教えてくれない。 | fn foo(x: u32) -> u32 { } - プログラム全体にわたる u32 のすべてのインスタンスに対して 1 つの ty::Ty があり、ty::Ty は u32 の両方の使用が同じ型を意味することを教えてくれる。 |
fn foo(x: &u32) -> &u32 { } - ここでも 2 つの rustc_hir::Ty がある。参照のライフタイムは、特別なマーカー LifetimeKind::Implicit を使って rustc_hir::Ty に現れる。 | fn foo(x: &u32) -> &u32 { }- 単一の ty::Ty。その ty::Ty には隠されたライフタイムパラメータがある。 |
順序
HIR は AST から直接構築されるため、ty::Ty が生成される前に行われます。
HIR が構築された後、いくつかの基本的な型推論と型チェックが行われます。
型推論の間に、あらゆるものの ty::Ty が何であるかを突き止め、また何かの型が曖昧でないかも確認します。
その後、ty::Ty は、すべてが期待される型を持っていることを確認しながら型チェックに使用されます。
hir_ty_lowering モジュールには、rustc_hir::Ty を ty::Ty に lowering する責任を持つコードがあります。
使用される主なルーチンは lower_ty です。
これは型チェックフェーズ中に発生しますが、「この関数はどのような引数型を期待しているのか」のような質問をしたいコンパイラの他の部分でも発生します。
セマンティクスが Ty の 2 つのインスタンスを駆動する仕組み
HIR は、最も少ない仮定を置く型情報の視点として考えることができます。 2 つのものが同じものだと証明されるまでは、それらは別個のものだと仮定します。 言い換えると、それらについて知っていることが少ないため、それらについての仮定も少なくすべきです。
構文的には、それらは 2 つの文字列です。N 行 20 列の "u32" と、N 行 35 列の "u32" です。
それらが同じであることはまだわかっていません。
そのため、HIR ではそれらを異なるものとして扱います。
後になって、それらがセマンティクス上は同じ型であると判断し、それが使用する ty::Ty になります。
別の例として、fn foo<T>(x: T) -> u32 を考えてみましょう。
誰かが foo::<u32>(0) を呼び出したとします。
これは、この呼び出しにおいて T と u32 が実際には同じ型であることが最終的に判明するという意味なので、最終的には同じ ty::Ty に到達することになりますが、rustc_hir::Ty は別個のものです。
(ただし、これは少し単純化しすぎています。型チェック中には関数をジェネリックにチェックし、u32 とは別個の T が依然として存在するためです。
後でコード生成を行うときには、各関数の「単相化された」(完全に置換された)バージョンを常に扱うことになり、そのため T が何を表しているか(そして具体的にはそれが u32 であること)がわかります。)
もう 1 つ例を示します。
#![allow(unused)]
fn main() {
mod a {
type X = u32;
pub fn foo(x: X) -> u32 { 22 }
}
mod b {
type X = i32;
pub fn foo(x: X) -> i32 { x }
}
}
ここでは、型 X は明らかにコンテキストによって変化します。
rustc_hir::Ty を見ると、
どちらの場合も X はエイリアスであると返されます(ただし、名前解決によって
別々のエイリアスに対応付けられます)。
しかし、ty::Ty シグネチャを見ると、fn(u32) -> u32
または fn(i32) -> i32 のいずれかになります(型エイリアスは完全に展開されます)。
ty::Ty の実装
rustc_middle::ty::Ty は実際には
Interned<WithCachedTypeInfo<TyKind>> のラッパーです。
一般に Interned は無視してかまいません。基本的に明示的にアクセスすることはありません。
私たちは常にそれらを Ty の中に隠し、Deref の実装やメソッドを介して飛ばします。
TyKind は、多くの異なる Rust の型
(たとえばプリミティブ、参照、代数的データ型、ジェネリクス、ライフタイムなど)を表すバリアントを持つ大きな enum です。
WithCachedTypeInfo には、flags や outer_exclusive_binder のようないくつかのキャッシュされた値があります。
これらは
効率のための便利なハックであり、私たちが知りたい場合がある型に関する情報を要約していますが、
ここではそれほど重要ではありません。
最後に、Interned により、ty::Ty は薄いポインターのような
型になれます。
これにより、インターン化の他の利点に加えて、等価性の比較を低コストで行えます。
型の割り当てと操作
新しい型を割り当てるには、
Ty に定義されているさまざまな new_* メソッドを使用できます。
これらの名前は、おおむねさまざまな型の種類に対応しています。
例:
let array_ty = Ty::new_array_with_const_len(tcx, ty, count);
これらのメソッドはすべて Ty<'tcx> を返します。返されるライフタイムは、
この tcx がアクセスできるアリーナのライフタイムであることに注意してください。
型は常に正規化され、インターン化されます(そのため、まったく同じ型を二度割り当てることはありません)。
また、tcx 自体のフィールドにアクセスすることで、さまざまな一般的な型を見つけることもできます:
tcx.types.bool、tcx.types.char などです(詳細は CommonTypes を参照してください)。
型の比較
型はインターン化されているため、== を使って等価性を効率的に比較できます
— しかし、ハッシュ化して重複を探している場合でもない限り、これはほとんど決して望む操作ではありません。
これは、Rust では同じ型を表現する方法が複数あることが多く、
特に推論が関わるとそうなるためです。
たとえば、型 {integer}(ty::Infer(ty::IntVar(..)) は整数推論変数で、
0 のような整数リテラルの型)と u8(ty::UInt(..))は、
互いに代入可能かどうかをテストするときには等しいものとして扱われるべき場合がよくあります
(これは診断コードで一般的な操作です)。
ただし、それらに対する == は false を返します。なぜなら、それらは異なる型だからです。
2 つの型を正しく比較する最も単純な方法には、推論コンテキスト(infcx)が必要です。
それがある場合、infcx.can_eq(param_env, ty1, ty2) を使用して、
型を等しくできるかどうかを確認できます。
これは通常、診断中に確認したいことです。診断で関心があるのは、
2 つの型を互いに代入できるかどうかのような問いであり、
コンパイラの型検査レイヤーで同一に表現されているかどうかではないためです。
推論コンテキストを扱うときは、型の内部にある可能性のある推論変数が、 実際にその推論コンテキストに属していることを確認するよう注意する必要があります。 すでに推論コンテキストにアクセスできる関数内にいる場合、これは成り立つはずです。 具体的には、これは HIR 型検査中や MIR 借用検査中に当てはまります。
もう 1 つ考慮すべき点は正規化です。
2 つの型は実際には同じであっても、一方が関連型の背後にある場合があります。
それらを正しく比較するには、まず型を正規化する必要があります。
これは主に、HIR 型検査中、および TyCtxt クエリから得られるすべての型
(たとえば tcx.type_of() から得られる型)で問題になります。
型検査中に FnCtxt または ObligationCtxt が利用可能な場合は、型を正規化するために
それらに対して .normalize(ty) を使用するべきです。
型検査後、診断コードは tcx.normalize_erasing_regions(ty) を使用できます。
Ty に対して == を使って問題ない場合もあります。
これは、たとえば late lint の場合
やモノモーフィゼーション後に当てはまります。型検査が完了しているため、すべての推論変数が
解決され、すべての領域が消去されているからです。
このような場合、推論変数
または正規化が問題にならないことが分かっているなら、その lint を #[allow] または #[expect] することが推奨されます。
診断コードが推論コンテキストにアクセスできない場合、どこかで(型検査中などに) 利用可能な推論コンテキストがあるなら、それを関数呼び出しを通じて渡すべきです。
推論コンテキストがまったく利用できない場合は、
type-inference で説明されているように作成できます。
しかし、これは関係する型(たとえば、
tcx.type_of() のようなクエリから来た場合)が、実際に fresh_args_for_item を使用して
新しい推論変数で置換されている場合にのみ有用です。
これにより、「任意の T に対する Vec<T> は Vec<u32> と単一化できるか?」のような問いに答えることができます。
ty::TyKind のバリアント
注: TyKind は関数型プログラミングにおける Kind の概念ではありません。
コンパイラ内で Ty を扱うときは、その型の kind に対して match することが一般的です:
fn foo(x: Ty<'tcx>) {
match x.kind {
...
}
}
kind フィールドの型は TyKind<'tcx> であり、これはコンパイラ内のすべての異なる種類の
型を定義する enum です。
注意: 型推論中に型の
kindフィールドを検査するのは危険な場合があります。 推論変数や考慮すべき他のものがある場合があり、また型がまだ分かっておらず、 後で分かるようになる場合があるためです。
関連する型はたくさんあり、いずれ扱います(たとえば領域/ライフタイム、 「置換」など)。
TyKind enum には多くのバリアントがあり、その
ドキュメント を見ることで確認できます。
以下はその一部です:
- 代数的データ型 (ADT) 代数的データ型は、
struct、enum、またはunionです。 内部的には、struct、enum、unionは実際には 同じ方法で実装されています。これらはすべてty::TyKind::Adtです。 基本的にはユーザー定義型です。 これらについては後で詳しく説明します。 - Foreign
extern type Tに対応します。 - Str
str型です。 ユーザーが&strと書いた場合、Strはその型のstr部分を表す方法です。 - Slice
[T]に対応します。 - Array
[T; n]に対応します。 - RawPtr
*mut Tまたは*const Tに対応します。 - Ref
Refは安全な参照、&'a mut Tまたは&'a Tを表します。Refにはいくつかの 関連する部分があります。たとえば、Ty<'tcx>は参照が参照する型です。Region<'tcx>は参照のライフタイムまたはリージョンであり、Mutabilityは参照が ミュータブルかどうかを表します。 - Param 型パラメーター(例:
Vec<T>のT)を表します。 - Error どこかで発生した型エラーを表し、よりよい診断を出力できるようにします。 これについては後で詳しく説明します。
- そして他にも多数…
インポート規約
厳密なルールはありませんが、tyモジュールは次のように使われる傾向があります。
use ty::{self, Ty, TyCtxt};
特に、非常によく使われるため、Ty型とTyCtxt型は直接インポートされます。
他の
型は、多くの場合、明示的なty::プレフィックス付きで参照されます(例: ty::TraitRef<'tcx>)。ただし、一部の
モジュールでは、より多い、またはより少ない名前の集合を明示的にインポートすることを選びます。
型エラー
ユーザーが型エラーを起こしたときに生成されるTyKind::Errorがあります。
その考え方は、
この型を伝播させ、それによって発生する他のエラーを抑制することで、カスケードするコンパイラーエラーメッセージで
ユーザーを圧倒しないようにする、というものです。
TyKind::Errorには重要な不変条件があります。
コンパイラーは、エラーがすでにユーザーに報告されていることを知っている場合を除き、決してErrorを生成すべきではありません。
これは通常、
(a) その場で報告したばかりであるか、(b) 既存のError型を伝播している(その
場合、そのエラー型が生成されたときにエラーが報告されているはずである)ためです。
この不変条件を維持することが重要なのは、Error型の要点が他のエラーを抑制すること、つまりそれらを報告しないことにあるためです。実際には
ユーザーにエラーを出力せずにError型を生成してしまうと、その後のエラーが抑制される可能性があり、
コンパイルが意図せず成功してしまうかもしれません!
場合によっては第三のケースがあります。
エラーが報告されていると考えているものの、それは
ローカルではなく、コンパイルのもっと前の段階で報告されていたはずだと考えている場合です。
その場合、delayed_bugまたはspan_delayed_bugで「遅延バグ」を作成できます。
これにより、コンパイルがエラーを生成することを期待している、という記録が残されます。ただし、
コンパイルが成功してしまった場合は、コンパイラーバグ報告がトリガーされます。
安全性を高めるため、実際にはrustc_middle::tyの外部で
TyKind::Error値を生成することはできません。TyKind::Errorには、他の場所で構築できないようにする
プライベートメンバーがあります。
代わりに、
Ty::new_errorメソッドまたはTy::new_error_with_messageメソッドを使うべきです。
これらのメソッドは、ErrorGuaranteedを受け取るか、
またはspan_delayed_bugを呼び出してから、種類がErrorのインターン済みTyを返します。
すでにspan_delayed_bugを使う予定だった場合は、冗長な遅延バグを避けるために、代わりに
spanとメッセージをty_error_with_messageに渡すだけで済みます。
TyKindバリアントの省略記法
Tyのデバッグ出力を見るときや、単にコンパイラー内のさまざまな型について話すときに、有効なRustではないものの、型に関する内部情報を簡潔に表すために使われる構文に遭遇することがあります。
以下は、さまざまな構文が実際に何を意味するのかを示すクイックリファレンスのチートシートです。
- ジェネリックパラメーター:
{name}/#{index}例:T/#0。ここでindexはジェネリックパラメーターのリスト内での位置に対応します - 推論変数:
?{id}例:?x/?0。ここでidは推論変数を識別します - バインダー由来の変数:
^{binder}_{index}例:^0_x/^0_2。ここでbinderとindexは、どのバインダーのどの変数が参照されているかを識別します - プレースホルダー:
!{id}または!{id}_{universe}例:!x/!0/!x_2/!0_2。指定されたユニバース内の何らかの一意な型を表します。ユニバースが0の場合、多くの場合は省かれます
これらについては後の章でより詳しく扱われるはずです。