상수 제네릭
상수 인자의 종류
존재하는 ty::Const 종류 대부분은 기존 타입 종류와 직접 대응합니다. 예를 들어 ConstKind::Param은 TyKind::Param에 해당합니다.
여기서 주목할 만한 주요 사항은 다음과 같습니다.
ConstKind::Unevaluated는TyKind::Alias에 해당하며, 장기적으로는 이름을 바꾸어야 합니다(ty::AliasKind에 대응하는AliasConstKind의 도입도 함께 필요합니다).ConstKind::Value는 단형화 이후ty::Const의 최종 값입니다. 이는TyKind::Str나TyKind::ADT처럼 완전히 구체적인 것들과 다소 비슷합니다.
상수 인자의 모든 종류와 이들이 타입 시스템에서 실제로 어떻게 표현되는지에 대한 전체 목록은 ConstKind 타입을 참고하십시오.
추론 변수는 별로 특별할 것이 없으며, 거의 모든 곳에서 타입 추론 변수와 동일하게 취급됩니다. 상수 매개변수 역시 마찬가지로 특별할 것이 없으며, 거의 모든 곳에서 타입 매개변수의 사용과 동일합니다. 그러나 파싱, 이름 해석, AST 로워링 과정에서 이들을 처리하는 방식에는 몇 가지 흥미롭고 미묘한 점이 있습니다: ambig-unambig-ty-and-consts.
익명 상수
익명 상수(anonymous 상수 항목의 줄임말)는 상수 제네릭에서 임의의 표현식을 나타내는 방식으로, 예를 들어 1 + 1, foo() 또는 그냥 0인 배열 길이가 이에 해당합니다. 이는 상수 제네릭에만 있는 것으로, 타입에는 실제로 이에 대응하는 것이 없습니다.
디슈가링
#![allow(unused)]
fn main() {
struct Foo<const N: usize>;
type Alias = [u8; 1 + 1];
}
이 예제에는 익명 상수로 표현되는 1 + 1이라는 상수 인자(배열 길이)가 있습니다. 디슈가링하면 대략 다음과 같습니다.
#![allow(unused)]
fn main() {
struct Foo<const N: usize>;
const ANON: usize = 1 + 1;
type Alias = [u8; ANON];
}
여기서 [u8; ANON]의 배열 길이는 그 자체로 ANON의 사용을 담은 익명 상수가 아니라, ANON 상수 항목을 일종의 “직접” 사용하는 것(ConstKind::Unevaluated)입니다.
익명 상수는 자신이 속한 항목의 제네릭 매개변수를 전혀 상속받지 않습니다.
#![allow(unused)]
fn main() {
struct Foo<const N: usize>;
type Alias<T: Sized> = [T; 1 + 1];
// Desugars To;
struct Foo<const N: usize>;
const ANON: usize = 1 + 1;
type Alias<T: Sized> = [T; ANON];
}
Alias에는 타입 매개변수 T와 where 절 T: Sized가 모두 있지만, ANON 상수에는 제네릭 매개변수도 where 절도 없다는 점에 주목하십시오. 이 디슈가링은 익명 상수가 제네릭 매개변수를 사용할 수 없도록 강제하는 방법의 일부입니다.
익명 상수가 실제 상수 항목으로 디슈가링된다고 생각하면 이해에 도움이 되지만, 컴파일러가 실제로 이런 방식으로 구현되어 있지는 않습니다.
AST 로워링 시점에는 아직 익명 상수의 타입을 알지 못하므로, 타입을 명시적으로 작성한 실제 HIR 항목으로 디슈가링할 수 없습니다. 이를 해결하기 위해 DefKind::AnonConst와 hir::Node::AnonConst가 있으며, 이들은 실제로는 디슈가링할 수 없는 이러한 익명 상수 항목을 나타내는 데 사용됩니다.
이러한 익명 상수의 타입은 type_of 쿼리로 얻을 수 있습니다. 그러나 type_of 쿼리에는 실제로 타입을 계산하는 로직이 들어 있지 않습니다(사실 호출하면 그냥 ICE가 발생합니다). 대신 HIR Ty 로워링이 로워링되는 모든 익명 상수에 대해 type_of 쿼리의 값을 주입하는 역할을 맡습니다. HIR Ty 로워링은 익명 상수가 인자로 전달되는 상수 매개변수의 타입을 보고 익명 상수의 타입을 결정할 수 있습니다.
TODO: 쿼리 주입에 관한 장을 작성하고 여기에 링크하십시오.
어떤 의미에서 앞의 예제들을 디슈가링한 결과는 다음과 같습니다.
#![allow(unused)]
fn main() {
struct Foo<const N: usize>;
type Alias = [u8; 1 + 1];
// sort-of desugars to pseudo-rust:
struct Foo<const N: usize>;
const ANON = 1 + 1;
type Alias = [u8; ANON];
}
Alias의 배열 타입에 대해 HIR ty 로워링을 수행할 때 배열 길이도 함께 로워링하며, type_of(ANON) -> usize를 주입합니다. 결과적으로 ANON 상수 항목의 타입은 HIR을 구성할 때가 아니라 컴파일러의 이후 단계 중 어딘가에서 설정됩니다.
이러한 디슈가링이 모두 끝나면, 타입 시스템에서의 최종 표현(즉 ty::Const로서의 표현)은 AnonConst의 DefId를 가진 ConstKind::Unevaluated입니다. 이는 익명 상수를 거치지 않고 실제 상수 항목의 사용을 표현한다면(예: min_generic_const_args가 활성화된 경우) 그것을 표현하는 방식과 동일합니다.
이를 통해 const “별칭“의 표현을 TyKind::Alias의 표현과 동일하게 만들 수 있습니다. 제대로 된 HIR 본문이 있으면 많은 코드를 재사용할 수도 있습니다. 예를 들어 HIR 타입 검사와 MIR로의 모든 로워링 단계를 재사용할 수 있으며, MIR에서는 다시 const 평가를 재사용할 수 있습니다.
제네릭 매개변수 부재 강제하기
익명 상수가 제네릭 매개변수를 사용할 수 없도록 강제하는 방법은 세 가지입니다:
- 이름 해석은 익명 상수 내부에서는 경로를 제네릭 매개변수로 해석하지 않습니다
- HIR Ty 로워링은 익명 상수 내부에서 제네릭 매개변수를 참조하는 타입에 대한
Self타입 별칭을 만나면 오류를 냅니다 - 익명 상수는 부모 정의로부터 where 절이나 제네릭을 상속하지 않습니다(즉
generics_of에는 익명 상수의 부모가 포함되지 않습니다)
#![allow(unused)]
fn main() {
// *1* Errors in name resolution
type Alias<const N: usize> = [u8; N + 1];
//~^ ERROR: generic parameters may not be used in const operations
// *2* Errors in HIR Ty lowering:
struct Foo<T>(T);
impl<T> Foo<T> {
fn assoc() -> [u8; { let a: Self; 0 }] {}
//~^ ERROR: generic `Self` types are currently not permitted in anonymous constants
}
// *3* Errors due to lack of where clauses on the desugared anon const
trait Trait<T> {
const ASSOC: usize;
}
fn foo<T>() -> [u8; <()>::ASSOC]
//~^ ERROR: no associated item named `ASSOC` found for unit type `()`
where
(): Trait<T> {}
}
두 번째 사항은 특히 미묘합니다. HIR Ty 로워링을 잘못 구현하여 익명 상수가 제네릭 매개변수를 사용할 수 없다는 점을 제대로 강제하지 못하기가 매우 쉽기 때문입니다. 기존 검사는 지나치게 보수적이면서도, 일부 제네릭 매개변수가 익명 상수의 본문에 들어가는 것을 의도치 않게 허용합니다 #144547.
익명 상수에서 제네릭 매개변수를 잘못 허용하면 때때로 ICE가 발생할 수 있지만, 잘못된 형식의 프로그램을 받아들이는 결과로 이어질 수도 있습니다.
세 번째 사항 역시 다소 미묘합니다. 부모 항목의 where 절을 하나도 상속하지 않으므로, 제네릭 매개변수를 언급하는 스코프 내 where 절에 근거하여 트레이트 해결이 추론 변수를 제네릭 매개변수로 추론하는 상황이 생길 수 없습니다. 예를 들어, <() as Trait<?x>>::ASSOC 표현식과 스코프 내의 (): Trait<T> where 절로부터 ?x=T를 추론하는 경우입니다.
또한 이렇게 하면, 익명 상수에서 제네릭 매개변수를 실제로 의도치 않게 허용하게 되더라도 컴파일러가 ICE를 일으키거나 적어도 부수적으로 어떤 종류의 오류를 낼 가능성이 훨씬 높아집니다. 익명 상수의 환경에는 제네릭 매개변수를 제대로 처리하는 데 필요한 정보가 전혀 없기 때문입니다.
배열 반복 표현식
위의 모든 내용에 대한 유일한 예외는 배열 표현식의 반복 횟수입니다. 하위 호환성을 위한 임시방편으로, 반복 횟수 상수 인자에서는 제네릭 매개변수를 사용할 수 있도록 허용합니다.
#![allow(unused)]
fn main() {
fn foo<T: Sized>() {
let a = [1_u8; size_of::<T>()];
}
}
그러나 익명 상수 상수 인자에서 제네릭 매개변수를 허용할 때 생기는 문제 대부분을 피하기 위해, 해당 상수가 단형화 이전(예: 타입 검사 중)에 평가되어야 한다고 요구합니다. 어떤 의미에서는 제네릭 매개변수가 의미상 사용되지 않는 경우에만 여기서 허용하는 것입니다.
앞의 예시에서 익명 상수는 어떤 타입 매개변수 T에 대해서도 평가할 수 있습니다. 크기가 정해진 타입에 대한 원시 포인터는 항상 같은 크기(예: 64비트 플랫폼에서는 8)를 가지기 때문입니다.
구문상으로는 제네릭 매개변수를 포함하지만 실제로는 평가가 성공하는 데 그것들에 의존하지 않은 익명 상수를 평가했음을 감지하면, const_evaluatable_unchecked FCW를 발생시킵니다. 이는 상수 인자에서 제네릭 매개변수를 사용하는 더 많은 방식, 예를 들어 min_generic_const_args나 (현재는 폐기된) generic_const_exprs가 안정화되면 하드 오류가 될 예정입니다.
이 FCW의 구현은 여기에서 찾을 수 있습니다: const_eval_resolve_for_typeck
generic_const_parameter_types와의 비호환성
const N: [u8; M]이나 const N: Foo<T>와 같은 상수 매개변수를 지원하는 것은 현재의 익명 상수 구조와 잘 맞지 않습니다. 여기에는 두 가지 이유가 있습니다:
-
익명 상수는 제네릭 매개변수를 사용할 수 없으므로, 그 타입 또한 제네릭 매개변수를 참조할 수 없습니다. 이는 타입이 여전히 제네릭 매개변수를 참조하는 상수 매개변수의 인자로 익명 상수를 사용하는 것이 근본적으로 불가능하다는 뜻입니다.
#![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>() { // There is no way to specify the const argument to `M` foo::<N, { [1_u8; N] }>(); } } -
현재는 HIR ty 로워링 중에 익명 상수를 로워링할 때 그 타입을 알고 있어야 합니다. 제네릭 상수 매개변수 타입이 있으면, 현재 알려진 타입에 추론 변수가 포함되어 있을 수 있습니다(즉, 아직 완전히 알려지지 않았을 수 있습니다).
#![allow(unused)] #![feature(adt_const_params, generic_const_parameter_types)] fn main() { fn foo<const N: usize, const M: [u8; N]>() {} fn bar() { // The const argument to `N` must be explicitly specified // even though it is able to be inferred foo::<_, { [1_u8; 3] }>(); } }
generic_const_parameter_types가 나머지 상수 제네릭과 잘 맞물려 동작하게 만드는 올바른 방법이 무엇인지는 현재 명확하지 않습니다.
generic_const_exprs를 사용하면 제네릭 매개변수를 참조하는 타입을 가진 익명 상수를 허용할 수 있었겠지만, 그 설계는 결국 실현할 수 없는 것으로 드러났습니다.
min_generic_const_args는 일부 표현식(예: 배열 생성)을 익명 상수 없이 표현할 수 있게 하여 이러한 문제를 피할 수 있게 해 줄 것입니다. 다만 이것으로 충분한지는 아직 결정되지 않았습니다.
상수 인자의 타입 검사
상수 인자가 적격이려면, 그것이 인자로 전달되는 상수 매개변수와 같은 타입을 가져야 합니다. 예를 들어, 배열의 길이 매개변수는 usize 타입을 가지므로, 배열 길이에 대한 bool 타입의 상수 인자는 적격이 아닙니다.
#![allow(unused)]
fn main() {
type Alias<const B: bool> = [u8; B];
//~^ ERROR:
}
이를 검사하기 위해 ClauseKind::ConstArgHasType(ty::Const, Ty)가 있으며, 항목에 정의된 각 상수 매개변수마다 그에 상응하는 ConstArgHasType 절을 해당 항목의 where 절 목록으로 디슈가링합니다. 이렇게 하면 어떤 것의 모든 절을 증명하여 적격성을 검사할 때마다, 모든 상수 인자가 올바른 타입을 가지는지도 함께 검사하게 됩니다.
#![allow(unused)]
fn main() {
fn foo<const N: usize>() {}
// desugars to in pseudo-rust
fn foo<const N>()
where
// ConstArgHasType(N, usize)
N: usize, {}
}
ConstArgHasType 목표의 증명은 먼저 상수 인자의 타입을 계산한 다음, 이를 주어진 타입과 동일시하는 방식으로 구현됩니다. 상수 인자의 타입을 계산하는 방식의 대략적인 개요는 다음과 같습니다.
ConstKind::Param(N)은ParamEnv에서 조회하여ConstArgHasType(N, ty)절을 찾을 수 있습니다ConstKind::Value는 값의 타입을 자체적으로 저장하므로 간단히 접근할 수 있습니다ConstKind::Unevaluated는type_of쿼리를 호출하여 타입을 계산할 수 있습니다- 더 자세한 내용은
ConstArgHasType목표를 증명하는 구현을 참고하십시오
ConstArgHasType는 상수 인자가 올바른 타입을 가지는지 검사하는, 건전성에 핵심적인 바로 그 방식입니다. 그러나 일부 경우에는 상수 인자의 타입을 다른 방식으로 간접적으로 검사하기도 합니다.
#![allow(unused)]
fn main() {
type Alias = [u8; true];
// desugars to
const ANON: usize = true;
type Alias = [u8; ANON];
}
익명 상수의 타입을 상수 매개변수의 타입으로 공급함으로써, 해당 익명 상수가 관련된 ConstArgHasType 목표가 성공함을 보장합니다. 익명 상수의 타입이 상수 매개변수의 타입과 일치하지 않는 경우, 실제로는 익명 상수의 본문을 타입 검사할 때 타입 검사 오류가 발생합니다.
위 예제에 비추어 보면, 이는 ANON이 usize 타입을 가지므로 [u8; ANON]은 적격 타입이지만, usize 타입의 상수 항목에서 true를 반환할 수 없으므로 ANON의 본문은 적격하지 않아 타입 검사 오류가 발생하는 것에 해당합니다.