constジェネリクス
const引数の種類
存在する ty::Const の種類のほとんどは、存在する型の種類と直接対応しています。たとえば ConstKind::Param は TyKind::Param に相当します。
ここで主に興味深い点は次のとおりです。
ConstKind::Unevaluated。これはTyKind::Aliasに相当し、長期的には名前を変更するべきです(また、ty::AliasKindに対応するAliasConstKindも導入するべきです)。ConstKind::Value。これは単相化後のty::Constの最終的な値です。 これはTyKind::StrやTyKind::ADTのような完全に具体的なものにやや似ています。
const引数のすべての種類と、それらが型システムで実際にどのように表現されるかの完全な一覧については、ConstKind 型を参照してください。
推論変数はかなり面白みがなく、ほとんどあらゆる場所で型推論変数と同等に扱われます。 constパラメータも同様に面白みがなく、ほとんどあらゆる場所で型パラメータの使用と同等です。 ただし、構文解析、名前解決、AST lowering中にそれらがどのように扱われるかには、いくつか興味深い微妙な点があります: ambig-unambig-ty-and-consts。
Anon Consts
Anon Consts(anonymous const itemsの略)は、constジェネリクスで任意の式を表現する方法です。たとえば、配列長の 1 + 1 や foo()、あるいは単なる 0 などです。
これらはconstジェネリクスに固有であり、型における実質的な相当物はありません。
脱糖
#![allow(unused)]
fn main() {
struct Foo<const N: usize>;
type Alias = [u8; 1 + 1];
}
この例では、1 + 1 というconst引数(配列長)があり、これは anon const として表現されます。脱糖すると、おおよそ次のようになります。
#![allow(unused)]
fn main() {
struct Foo<const N: usize>;
const ANON: usize = 1 + 1;
type Alias = [u8; ANON];
}
ここで、[u8; ANON] の配列長は、ANON の使用を含むanon constそのものではなく、ANON constアイテムの「直接的」な使用の一種です(ConstKind::Unevaluated)。
anon constは、それが含まれているアイテムのジェネリックパラメータを継承しません。
#![allow(unused)]
fn main() {
struct Foo<const N: usize>;
type Alias<T: Sized> = [T; 1 + 1];
// 脱糖すると次のようになる;
struct Foo<const N: usize>;
const ANON: usize = 1 + 1;
type Alias<T: Sized> = [T; ANON];
}
Alias には型パラメータ T とwhere句 T: Sized の両方がありますが、ANON constにはジェネリックパラメータもwhere句もないことに注目してください。
この脱糖は、anon constがジェネリックパラメータを利用できないことを強制する仕組みの一部です。
anon constを実際のconstアイテムへ脱糖されるものとして考えるのは有用ですが、コンパイラは実際にはこの方法で実装していません。
AST loweringの時点では、anon constの型がまだわかっていないため、明示的に書かれた型を持つ実際のHIRアイテムへ脱糖することはできません。
これを回避するために、DefKind::AnonConst と hir::Node::AnonConst があります。
これらは、実際には脱糖できないこのような匿名constアイテムを表現するために使われます。
これらのanon constの型は、type_of クエリから取得できます。
しかし、type_of クエリには実際には型を計算するロジックは含まれていません(実際、呼び出されると単にICEします)。
代わりに、HIR Ty loweringが、loweringされた任意のanon constについて type_of クエリの値を供給する責任を負います。
HIR Ty loweringは、anon constが引数として渡されるconstパラメータの型を見ることで、そのanon constの型を決定できます。
TODO: query feedingに関する章を書いてここにリンクする
ある意味では、前の例の脱糖は次のようなものです。
#![allow(unused)]
fn main() {
struct Foo<const N: usize>;
type Alias = [u8; 1 + 1];
// だいたい次のような擬似Rustへ脱糖される:
struct Foo<const N: usize>;
const ANON = 1 + 1;
type Alias = [u8; ANON];
}
Alias 内の配列型についてHIR ty loweringを通るとき、配列長もloweringし、type_of(ANON) -> usize を供給します。
これにより、HIRを構築するときではなく、コンパイラの後の段階で ANON constアイテムの型を実質的に設定することになります。
この脱糖がすべて行われた後、型システム内の最終的な表現(すなわち ty::Const として)は、AnonConst の DefId を持つ ConstKind::Unevaluated になります。これは、anon constを経由せずに実際のconstアイテムの使用を表現する場合(たとえば min_generic_const_args が有効な場合)に、それを表現する方法と同等です。
これにより、constの「エイリアス」の表現を TyKind::Alias の表現と同じにできます。適切なHIR本体があることにより、大量のコード再利用も可能になります。たとえば、HIR型検査や、MIRへのすべてのloweringステップを再利用でき、そこでさらにconst評価を再利用できます。
ジェネリックパラメータを使用できないことの強制
anon constがジェネリックパラメータを使用できないことを強制する方法は3つあります。
- 名前解決は、anon const内にいるとき、ジェネリックパラメータへのパスを解決しません
- HIR Ty loweringは、ジェネリックパラメータを参照する型への
Self型エイリアスにanon const内で遭遇したときにエラーにします - Anon Constsは、親定義からwhere句やジェネリクスを継承しません(すなわち
generics_ofはanon constについて親を含みません)
#![allow(unused)]
fn main() {
// *1* 名前解決でエラー
type Alias<const N: usize> = [u8; N + 1];
//~^ ERROR: ジェネリックパラメータはconst演算で使用できません
// *2* HIR Ty loweringでエラー:
struct Foo<T>(T);
impl<T> Foo<T> {
fn assoc() -> [u8; { let a: Self; 0 }] {}
//~^ ERROR: ジェネリックな`Self`型は現在、匿名定数では許可されていません
}
// *3* 脱糖されたanon constにwhere句がないことによるエラー
trait Trait<T> {
const ASSOC: usize;
}
fn foo<T>() -> [u8; <()>::ASSOC]
//~^ ERROR: ユニット型`()`に`ASSOC`という名前の関連アイテムが見つかりません
where
(): Trait<T> {}
}
2つ目の点は特に微妙です。HIR Ty loweringを誤って実装し、anon constがジェネリックパラメータを使用できないことを適切に強制し損ねるのは非常に簡単だからです。 既存のチェックは保守的すぎる一方で、誤って一部のジェネリックパラメータがanon constの本体に入り込むことを許してしまいます #144547。
anon const内でジェネリックパラメータを誤って許可すると、ICEにつながることもありますが、不正な形式のプログラムを受理することにもつながり得ます。
3つ目の点もやや微妙です。親アイテムのwhere句を一切継承しないことで、スコープ内のwhere句がジェネリックパラメータに言及している場合に、トレイト解決がそのwhere句に基づいて推論変数をジェネリックパラメータへ推論してしまうことを避けられます。
たとえば、式 <() as Trait<?x>>::ASSOC と、スコープ内の (): Trait<T> というwhere句から ?x=T を推論するような場合です。
これにより、仮にanon const内でジェネリックパラメータを誤って許可してしまった場合でも、コンパイラがICEするか、少なくとも偶発的に何らかのエラーを出す可能性がずっと高くなります。なぜなら、そのanon constには、ジェネリックパラメータを適切に扱うために環境内で必要な情報がまったく存在しないからです。
配列の繰り返し式
上記すべてに対する唯一の例外は、配列式の繰り返し回数です。 後方互換性のためのハックとして、繰り返し回数のconst引数がジェネリックパラメータを使用することを許可しています。
#![allow(unused)]
fn main() {
fn foo<T: Sized>() {
let a = [1_u8; size_of::<T>()];
}
}
ただし、anon const の const 引数でジェネリックパラメータを許可することに伴う問題の大半を避けるため、定数はモノモーフィゼーション前(たとえば型チェック中)に評価される必要があります。ある意味では、ここでジェネリックパラメータを許可するのは、それらが意味的に未使用である場合に限られます。
前の例では、サイズ付き型への raw pointer は常に同じサイズ(たとえば 64bit プラットフォームでは 8)を持つため、anon const は任意の型パラメータ T に対して評価できます。
構文上はジェネリックパラメータを含んでいるものの、評価が成功するために実際にはそれらに依存していない anon const を評価したことを検出すると、const_evaluatable_unchecked FCW を出力します。
これは、const 引数でジェネリックパラメータを使用する方法、たとえば min_generic_const_args や(現在は廃止された)generic_const_exprs がさらに安定化された時点で、hard error になることが意図されています。
この FCW の実装はここにあります: const_eval_resolve_for_typeck
generic_const_parameter_types との非互換性
const N: [u8; M] や const N: Foo<T> のような const パラメータのサポートは、現在の anon const の仕組みとはあまり相性がよくありません。
これには 2 つの理由があります:
-
anon const はジェネリックパラメータを使用できないため、その型もジェネリックパラメータを参照できません。 つまり、型がまだジェネリックパラメータを参照している const パラメータへの引数として anon const を使用することは、根本的に不可能です。
#![allow(unused)] #![feature(adt_const_params, generic_const_parameter_types)] fn main() { fn foo<const N: usize, const M: [u8; N]>() {} fn bar<const N: usize>() { // `M` への const 引数を指定する方法はない foo::<N, { [1_u8; N] }>(); } } -
現在、HIR ty lowering 中に anon const を lowering する際、anon const の型を知っている必要があります。 generic const parameter types では、現在わかっている型に推論変数が含まれている(つまり、まだ完全にはわかっていない)場合があります。
#![allow(unused)] #![feature(adt_const_params, generic_const_parameter_types)] fn main() { fn foo<const N: usize, const M: [u8; N]>() {} fn bar() { // 推論できるにもかかわらず、`N` への const 引数は // 明示的に指定する必要がある foo::<_, { [1_u8; 3] }>(); } }
generic_const_parameter_types を const generics の残りの部分とうまく連携させる正しい方法は、現時点では明らかではありません。
generic_const_exprs では、ジェネリックパラメータを参照する型を持つ anon const が許可されるはずでしたが、その設計は最終的に実行不可能であることがわかりました。
min_generic_const_args では、一部の式(たとえば配列の構築)を anon const なしで表現できるようになり、そのためこれらの問題に遭遇しなくなります。ただし、これで 十分 かどうかはまだ判断されていません。
Const 引数の型チェック
const 引数が well formed であるためには、その引数の対象である const パラメータと同じ型を持っている必要があります。
たとえば、配列長に対する型 bool の const 引数は well formed ではありません。配列の長さパラメータの型は usize だからです。
#![allow(unused)]
fn main() {
type Alias<const B: bool> = [u8; B];
//~^ ERROR:
}
これをチェックするために ClauseKind::ConstArgHasType(ty::Const, Ty) があり、
item 上で定義された各 Const Parameter について、
等価な ConstArgHasType 節をその where 節のリストに desugar します。
これにより、すべての節を証明することで何かの wellformedness をチェックするたびに、
すべての Const Arguments が正しい型を持っていることもたまたまチェックされるようになります。
#![allow(unused)]
fn main() {
fn foo<const N: usize>() {}
// pseudo-rust では次のように desugar される
fn foo<const N>()
where
// ConstArgHasType(N, usize)
N: usize, {}
}
ConstArgHasType goals の証明は、まず const 引数の型を計算し、それを与えられた型と等価にすることで実装されています。
Const Argument の型を計算する方法の大まかな概要:
ConstKind::Param(N)はParamEnvで検索してConstArgHasType(N, ty)節を見つけることができますConstKind::Valueは値の型を自身の内部に保持しているため、簡単にアクセスできますConstKind::Unevaluatedはtype_ofquery を呼び出すことで型を計算できます- より詳細な情報については、
ConstArgHasTypegoals の証明の実装を参照してください
ConstArgHasType は、Const Arguments が正しい型を持つことをチェックする、健全性にとって重要な唯一の方法です。
ただし、一部の場合には、別の方法で Const Arguments の型を 間接的に チェックしています。
#![allow(unused)]
fn main() {
type Alias = [u8; true];
// 次のように desugar される
const ANON: usize = true;
type Alias = [u8; ANON];
}
anon const の型に Const Parameter の型を与えることで、
anon const を含む ConstArgHasType goal が成功することを保証します。
anon const の型が Const Parameter の型と一致しない場合、
実際に起こるのは、anon const の本体を型チェックするときの 型チェック エラーです。
上の例を見ると、これは ANON が型 usize を持つため [u8; ANON] は well formed な型である一方、ANON の 本体 は illformed であり、型 usize の const item から true を返すことはできないため型チェックエラーになる、ということに対応します。