Opaque 型におけるリージョン推論の制限
この章では、opaque 型の隠れた型
Opaque<'a, 'b, .., A, B, ..> := SomeHiddenType
を定義する際に、そのジェネリック引数へ課しているさまざまな制限について説明します。
これらの制限は、opaque 型推論の最終ステップであるため、借用検査(ソース)で実装されています。
背景: 型および const ジェネリック引数
型引数については、2 つの制限が必要です。各型引数は (1) 型パラメーターであり、 (2) ジェネリック引数の中で一意でなければなりません。 同じことが const 引数にも適用されます。
ケース (1) の例:
#![allow(unused)]
fn main() {
type Opaque<X> = impl Sized;
// `T` は型パラメーターです。
// Opaque<T> := ();
fn good<T>() -> Opaque<T> {}
// `()` は型パラメーターではありません。
// Opaque<()> := ();
fn bad() -> Opaque<()> {} //~ ERROR
}
ケース (2) の例:
#![allow(unused)]
fn main() {
type Opaque<X, Y> = impl Sized;
// `T` と `U` はジェネリック引数内で一意です。
// Opaque<T, U> := T;
fn good<T, U>(t: T, _u: U) -> Opaque<T, U> { t }
// `T` はジェネリック引数内に 2 回出現しています。
// Opaque<T, T> := T;
fn bad<T>(t: T) -> Opaque<T, T> { t } //~ ERROR
}
動機: 最初のケース Opaque<()> := () では、隠れた型は 2 つの異なる解釈、すなわち Opaque<X> := X と Opaque<X> := () の両方に適合するため曖昧です。
同様に、2 番目のケース Opaque<T, T> := T では、Opaque<X, Y> := X として解釈すべきか、Opaque<X, Y> := Y として解釈すべきかが曖昧です。
この曖昧さのため、どちらのケースも無効な定義用法として拒否されます。
一意性の制限
各ライフタイム引数は、引数リスト内で一意でなければならず、'static であってはなりません。
これは、型パラメーターの場合と同様に、隠れた型の推論における曖昧さを避けるためです。
たとえば、以下の無効な定義用法 Opaque<'static> := Inv<'static> は、
Opaque<'x> := Inv<'static> と Opaque<'x> := Inv<'x> の両方に適合します。
#![allow(unused)]
fn main() {
type Opaque<'x> = impl Sized + 'x;
type Inv<'a> = Option<*mut &'a ()>;
fn good<'a>() -> Opaque<'a> { Inv::<'static>::None }
fn bad() -> Opaque<'static> { Inv::<'static>::None }
//~^ ERROR
}
#![allow(unused)]
fn main() {
type Opaque<'x, 'y> = impl Trait<'x, 'y>;
fn good<'a, 'b>() -> Opaque<'a, 'b> {}
fn bad<'a>() -> Opaque<'a, 'a> {}
//~^ ERROR
}
意味的なライフタイム等価性: 型パラメーターと比較した場合のライフタイムの複雑さの 1 つは、 構文上は異なる 2 つのライフタイムが、意味的には等しい場合があることです。 したがって、ライフタイムが一意であることを検証する際には注意が必要です。
#![allow(unused)]
fn main() {
// これも無効です。なぜなら `'a` は *意味的に* `'static` と等しいためです。
fn still_bad_1<'a: 'static>() -> Opaque<'a> {}
//~^ エラーになるはずです!
// これも無効です。なぜなら `'a` と `'b` は *意味的に* 等しいためです。
fn still_bad_2<'a: 'b, 'b: 'a>() -> Opaque<'a, 'b> {}
//~^ エラーになるはずです!
}
一意性ルールの例外
上記の一意性ルールの例外は、opaque 型の定義における境界が、あるライフタイムパラメーターを別のもの、または 'static ライフタイムと等しくすることを要求している場合です。
#![allow(unused)]
fn main() {
// 定義は `'x` が `'static` と等しいことを要求しています。
type Opaque<'x: 'static> = impl Sized + 'x;
fn good() -> Opaque<'static> {}
}
動機: RPIT に対して一意性の制限を実装しようとしたところ、 crater によって発見された破壊的影響が発生しました。 これは、このルールの例外によって緩和できます。 そうしないと破壊的影響を受けるコードの例:
#![allow(unused)]
fn main() {
struct Type<'a>(&'a ());
impl<'a> Type<'a> {
// `'b == 'a`
fn do_stuff<'b: 'a>(&'b self) -> impl Trait<'a, 'b> {}
}
}
これが正しい理由: Opaque<'a, 'a> := &'a str のような定義用法については、
Opaque<'x, 'y> := &'x str としても、Opaque<'x, 'y> := &'y str としても解釈でき、どちらでも問題ありません。なぜなら、Opaque のすべての使用は、well-formedness ルールに従って、両方のパラメーターが等しいことを保証するからです。
ユニバーサルライフタイムの制限
opaque 型引数では、普遍量化されたライフタイムのみが許可されます。 これにはライフタイムパラメーターとプレースホルダーが含まれます。
#![allow(unused)]
fn main() {
type Opaque<'x> = impl Sized + 'x;
fn test<'a>() -> Opaque<'a> {
// `Opaque<'empty> := ()`
let _: Opaque<'_> = ();
//~^ ERROR
}
}
動機:
これによりライフタイム引数と型引数の振る舞いに一貫性が生まれますが、それは副次的な利点にすぎません。
この制限の本当の理由は純粋に技術的なものであり、メンバー制約アルゴリズムが根本的な制約に直面しているためです。
opaque 型定義 Opaque<'?1> := &'?2 u8 に遭遇すると、
メンバー制約 '?2 member-of ['static, '?1] が登録されます。
アルゴリズムが正しい選択肢を選ぶには、選択肢リージョン ['static, '?1] 間の “outlives” 関係の完全な集合が、リージョン推論を行う前にすでに分かっていなければなりません。
これは、各選択肢リージョンが次のいずれかである場合にのみ満たせます。
- ユニバーサルリージョン、すなわち
RegionKind::Re{EarlyParam,LateParam,Placeholder,Static}。 なぜなら、ユニバーサルリージョン間の関係は、明示的境界と暗黙的境界から、リージョン推論に先立って完全に分かっているためです。 - または、ユニバーサルリージョンと「厳密に等しい」存在リージョン。 厳密なライフタイム等価性は以下で定義され、完全なリージョン推論より前に評価できる唯一の種類の等価性であるため、ここで必要になります。
厳密なライフタイム等価性: 2 つのライフタイムの間に双方向の outlives 制約がある場合、それらは厳密に等しいと言います。 NLL の用語では、これはライフタイムが同じ SCC の一部であることを意味します。 重要なのは、この種類の等価性は完全なリージョン推論の前に評価できることです (ただしもちろん、制約収集の後です)。 もう 1 つの種類の等価性は、リージョン推論の結果、2 つのライフタイム変数が、厳密には等しくなくても同じ値を与えられる場合です。 以前はこの違いを混同していたことについては、#113971 を参照してください。
“once modulo regions” 制限との相互作用
上の例では、シグネチャ内の opaque 型は Opaque<'a> であり、無効な定義用法内のものは Opaque<'empty> であることに注意してください。
提案されている MiniTAIT 計画、すなわち “once modulo regions” ルールでは、
これはすでに許可されていません。
「ユニバーサルライフタイム」の制限は「MiniTAIT」の制限から論理的に導かれるため冗長になるように見えるかもしれませんが、ライフタイム等価性とクロージャに関する後続の関連議論は引き続き重要です。
クロージャの制限
opaque 型がクロージャ/コルーチン/inline-const 本体内で定義されている場合、そのクロージャに対して「外部」であるユニバーサルライフタイムは、opaque 型引数では許可されません。
外部リージョンは [RegionClassification::External][source-external-region] で定義されています。
[source-external-region]: https://github.com/rust-lang/rust/blob/caf730043232affb6b10d1393895998cb4968520/compiler/rustc_borrowck/src/universal_regions.rs#L201.
例: (これは現在の nightly ではたまたまコンパイルされますが、より実用的な例は すでに分かりにくいエラーで reject されています。)
#![allow(unused)]
fn main() {
type Opaque<'x> = impl Sized + 'x;
fn test<'a>() -> Opaque<'a> {
let _ = || {
// `'a` はクロージャーの外部にある
let _: Opaque<'a> = ();
//~^ エラーになるべき!
};
()
}
}
動機: クロージャー本体では、外部ライフタイムは「ユニバーサル」ライフタイムとして分類されるものの、 それらの間の関係が事前には分からないという点で、存在ライフタイムに近い振る舞いをします。 代わりに、それらの値は存在ライフタイムと同様に推論され、要件は親 fn に伝播されます。 これは上で説明したメンバー制約アルゴリズムを壊します:
アルゴリズムが正しい選択肢を選べるようにするには、選択肢のリージョン
['static, '?1]間の 「outlives」関係の完全な集合が、リージョン推論を行う前にすでに分かっていなければならない
これがどのように起こるかを詳しく示す例を以下に示します:
#![allow(unused)]
fn main() {
type Opaque<'x, 'y> = impl Sized;
//
fn test<'a, 'b>(s: &'a str) -> impl FnOnce() -> Opaque<'a, 'b> {
move || { s }
//~^ ERROR `Opaque<'_, '_>` の隠れた型が、境界に現れないライフタイムをキャプチャしている
}
// 上のクロージャー本体は、おおよそ次のように desugar される:
fn test::{closure#0}(_upvar: &'?8 str) -> Opaque<'?6, '?7> {
return _upvar
}
// ここで `['?8, '?6, ?7]` はクロージャーの *外部* にあるユニバーサルライフタイムです。
// クロージャーの *内部* では、それらの間に既知の関係はありません。
// しかし親 fn では、`'?6: '?8` であることが分かっています。
//
// opaque 定義 `Opaque<'?6, '?7> := &'8 str` に遭遇したとき、
// メンバー制約アルゴリズムには `?8 = '?6` と安全に判断するための十分な情報がありません。
// このため、妥当なメッセージでエラーになります:
// "hidden type captures lifetime that does not appear in bounds".
}
これらの制限がないと、エラーメッセージが分かりにくくなり、さらに重要なこととして、 クロージャー内のメンバー制約は非常に壊れているため、将来壊れる可能性が高いコードを 受け入れてしまうリスクがあります。
出力型:
これが実際のコードで問題を引き起こす最も一般的なシナリオは、
クロージャー/async ブロックの出力型だと思います。クロージャーと async ブロックの間には、
この問題をさらに示す不一致があることに注意する価値があります。これは
future にのみ適用される
replace_opaque_types_with_inference_vars のハック
に起因しています。
#![allow(unused)]
fn main() {
type Opaque<'x> = impl Sized + 'x;
fn test<'a>() -> impl FnOnce() -> Opaque<'a> {
// クロージャーの出力型は Opaque<'a>
// -> 隠れた型の定義はクロージャーの *内部* で起こる
// -> reject される。
move || {}
//~^ ERROR ジェネリックなライフタイムパラメータが期待されたが、`'_` が見つかった
}
}
#![allow(unused)]
fn main() {
use std::future::Future;
type Opaque<'x> = impl Sized + 'x;
fn test<'a>() -> impl Future<Output = Opaque<'a>> {
// async ブロックの出力型はユニット `()`
// -> 隠れた型の定義は親 fn で起こる
// -> 受け入れられる。
async move {}
}
}