적격성
적격성이란 무엇입니까?
“적격(well-formed)“이란 “올바르게 구성된“이라는 뜻입니다1. 어떤 것의 구조가 규칙을 따를 때 그것은 적격합니다. Rust 컴파일러에서 이 용어를 사용할 때 우리가 관심을 두는 것은 일종의 내부 일관성을 확립하는 일입니다.
Rust의 적격성
어떤 것이 적격한지 확인하는 것을 “적격성 검사“를 수행한다고 합니다.
Rust 컴파일러에는 서로 다른 두 가지 형태의 적격성 검사가 있습니다.
- 타입 수준 항2 3 적격성 검사.
- “항 적격성” 또는 “항 적격성 검사“라고도 합니다.
- 별개의 분석 단계가 아니며, 분석 전반에 걸쳐 수행됩니다.
- 아이템4 적격성 검사(item-wfck).
- “Item-wfck“는 결국 항이 적격할 것을 요구하는 경우가 많지만, 일부 영역은 건너뜁니다.
- 내부의 “항“이 (잘못하여) 먼저 정규화될 수 있습니다.
- (여러 곳에서 수행되는) “항 적격성“보다 컴파일러의 한 단계로서 더 일관성이 있습니다.
참고: 적격성이 아닌 것.
타입 수준 항의 적격성
항 적격성 검사는 항이 적격하기 위해 참이어야 하는 것들의 목록을 만드는 것으로 시작합니다. 우리는 이것들을 “의무(Obligations)“라고 부릅니다5.
타입 수준 항은 연관된 의무가 트레이트 솔버에 의해 충족될 때 적격한 것으로 간주됩니다.
적격성을 위한 의무
구체적인 의무로는 String: Clone, A: usize, <T as Iterator>::Item: Debug 같은 것이 있습니다.
이 페이지에서는 의무와 항/아이템 사이의 구분을 다음과 같이 나타냅니다.
<terms or items>
---
<obligations>
다음은 적격한 타입 수준 항의 예입니다.
Vec<String>
---
// Obligations to fulfill
Vec<T> where T: Sized
// Trait solver says `String: Sized` is true, so this is well-formed.
Vec<String> where String: Sized
Vec<String>에 대한 의무를 계산하면 Vec<T>가 T: Sized라는 의무를 생성한다는 것을 알게 됩니다. Vec<String>에서 T를 String으로 치환하므로 String: Sized라는 의무를 얻게 되며, 트레이트 솔버는 이 의무가 충족된다고 판단합니다.
다음은 적격하지 않습니다.
Vec<str>
---
// Obligations to fulfill
Vec<T> where T: Sized
// Trait solver says `str: Sized` is not true, so this is not well-formed.
Vec<str> where str: Sized
위의 예는 앞에서와 마찬가지로 T: Sized라는 의무를 계산하지만, Vec<str> 인스턴스에서 T를 str로 치환하므로 str: sized라는 의무를 얻게 됩니다. 트레이트 솔버는 이 의무가 충족되지 않는다고 판단합니다.
의무 결정하기
컴파일러에서 항의 의무는 항 적격성 모듈의 obligations 함수를 통해 찾습니다.
기타 의무
의무에는 트레이트 바운드와 상수 제네릭 바운드 외에도 여러 종류가 있지만, 지금까지는 항의 “적격성 검사“를 수행할 때 관심을 두는 이 특정 의무들만 언급했습니다. 의무의 전체 목록은 PredicateKind와 ClauseKind를 참고하십시오.
정규화는 (아직) 필요하지 않습니다
정규화는 타입 별칭을 그 기반 타입으로 해석하는 과정입니다.
타입 별칭은 where 절이 충족되면 적격한 것으로 간주됩니다. 기반 타입은 대부분의 정의 위치와 인스턴스화 위치에서 적격성 검사를 거치지만, 예외도 있습니다.
상수 제네릭 인자
항 적격성은 상수 제네릭 항6의 “타입 검사” 의무를 얻는 역할을 담당합니다. 다음과 같은 상수 제네릭 사용 예를 살펴봅시다:
fn use_const_generics<const U: usize>() { /* ... */ }
// call site
use_const_generics::<6>();
---
// call site wfck obligations
const 6: usize
적격성 검사 중에 호출 위치는 6: usize라는 의무를 제공합니다. 트레이트 솔버는 이름이 암시하는 것보다 더 많은 역할을 맡고 있으므로, 이 의무는 다른 트레이트 형태의 의무와 마찬가지로 트레이트 솔버에 전달됩니다.
아이템의 적격성
아이템은 일반적으로 말해 “정의되는 것들“입니다. 아이템 적격성 검사(item-wfck)는 타입과 함수, 메서드, 그리고 트레이트의 정의/구현에 대해 시그니처 수준에서 이루어집니다.
// The `Vec<str>` is checked during item wfck
fn foo(_: Vec<str>) {
// The `Vec<[u8]>` is not handled by item wfck as it's not in the signature
let _: Vec<[u8]>
}
---
Vec<str>: Sized // Generated
Vec<[u8]>: Sized // Not done at item-wfck. Done elsewhere.
item-wfck는 내부 타입 수준 항의 의무를 수집하여 트레이트 솔버에 전달하는 것 외에도 더 많은 역할을 맡고 있습니다. 여기서 이 역할을 모두 다루지는 않지만, item-wfck 모듈의 개별 check_* 함수에서 찾아볼 수 있습니다.
전역 바운드와 자명한 바운드
트레이트 바운드는 흔한 의무입니다. 전역(Global) 트레이트 바운드와 자명한(Trivial) 트레이트 바운드는 참인지 거짓인지 판단하기에 충분한 정보를 이미 가지고 있는 트레이트 바운드의 종류입니다. item-wfck는 이러한 바운드를 찾아 검사하는 역할을 담당합니다.
- 전역 바운드는 기존 솔버에서 정규화 이후에 제네릭 매개변수(
<T>나'a등)나 바운드 변수(for<'b>등)를 포함하지 않는 바운드입니다. - 자명한 바운드는 적격인지 여부를 판단하는 데 추가적인 정규화가 필요하지 않은 바운드입니다.
다음 함수 정의를 살펴보십시오.
fn apartment_complex<T>(block: T, name: String) where String: Clone { /* ... */ }
---
String: Clone // Trivial & Global bound! There's no aliases to resolve.
// There could be bligations on T but we don't care about them here.
이 정의는 전역(제네릭 매개변수 없음)이면서 자명한(적격성 검사에 정규화가 필요하지 않음) 트레이트 바운드 의무 String: Clone을 생성합니다. 트레이트 솔버가 String: Clone의 적격성을 판단하는 데 추가적인 정보를 제공받을 필요는 없습니다.
거짓인 자명한 바운드는 단순히 성립하지 않는 자명한 바운드입니다. 다음은 기본적인 예입니다.
fn apartment_simple<T>(block: T, name: String) where String: Copy { /* ... */ }
---
String: Copy // Trivial bound again, but this one is false!
여기서는 String이 Copy가 아니므로 성립하지 않는 자명한 바운드가 있습니다.
자명한 바운드가 항상 전역 바운드인 것은 아닙니다
자명한 바운드는 전역 바운드의 부분집합이 아닙니다. 전역이 아닌 자명한 바운드의 예로는 for<'a> String: Clone(자명하게 참이며 바운드 변수를 가짐)이나 &'a str: Copy(자명하게 거짓이며 제네릭 매개변수를 가짐)가 있습니다.
item-wfck와 자명한/전역 바운드
항목이 적격한지 검사할 때 자명하게 거짓인 전역 바운드가 없는지 검사합니다.
적격성 검사를 완전히 수행하지 않는 경우
적격성 검사는 타입 검사의 일관된 하나의 “단계“가 아닙니다. 적격성 검사가 수행되는 영역은 많으며, 현재 수행할 수 있는 분석의 종류에 한계가 있어 적격성 검사를 건너뛰는 영역도 일부 있습니다. 이상적으로는 적격성 검사를 절대 건너뛰거나 미루지 않아야 합니다.
(때로는) 정규화가 필요합니다
항목의 항이 적격성 검사를 거치기 전에 해당 항목의 정규화가 일어나는 곳이 있습니다. 이렇게 하면 일부 항이 항 적격성 검사를 완전히 우회할 수 있으므로 문제가 있는 것으로 간주됩니다.
트레이트 객체
트레이트 객체가 적격한지 판단할 때 해당 트레이트 객체의 where 절이 적격할 것을 요구하지 않습니다. 이러한 where 절은 트레이트 객체로 강제 변환할 때 증명되지만, 이는 여전히 적격성 검사의 허점으로 남아 있습니다.
예를 들어, 다음 코드는 구체 타입으로부터 트레이트 객체를 생성하는 지점이 없기 때문에 컴파일됩니다:
trait Trait
where
for<'a> [u8]: Sized {}
fn foo(_: &dyn Trait) {}
---
// This doesn't end up being generated, because it happens within a trait object.
[u8]: Sized
위 코드는 [u8]: Sized 때문에 컴파일되지 않아야 하지만, 이는 실제로 사용될 때까지 검사되지 않습니다:
trait Trait
where
for<'a> [u8]: Sized {}
fn foo(_: &dyn Trait) {}
// We still need to specify the bound here, otherwise `[u8]: Sized` _is_
// checked as an obligation.
impl Trait for u8 where for<'a> [u8]: Sized {}
fn main() {
// No matter what we do, this boundary between concrete type and trait
// object will produce the obligation `[u8]: Sized`, which will fail when
// handed over to the trait solver.
let object: Box<dyn Trait> = Box::new(42u8);
foo(&object);
}
이 예외는 트레이트 객체 안의 상수 제네릭 인자에는 적용되지 않습니다:
trait Trait<const N: usize> {}
fn foo<const B: bool>(_: &dyn Trait<B>) {}
---
const N: usize
const B: bool
N = B // Substitution
const B: usize + bool
앞서 제시한 예제와 달리 위 코드는 컴파일되지 않습니다. 상수 제네릭 인자에 관해서는 여기서 어느 정도 적격성 검사를 수행합니다.
바인더 / 고차 타입
바인더 / 고차 타입은 항에 대해 수행하는 적격성 검사의 양을 줄이며, 적격성 검사를 바운드가 인스턴스화되는 시점으로 미룹니다:
let _: for<'a> fn(Vec<[&'a ()]>);
---
// This doesn't end up being generated, because it happens within a HRB
[&'a ()]: Sized // slices aren't sized, this would fail!
구체적으로, 바인더(for<'a>)의 변수가 관련된 의무(obligation)는 바인더가 인스턴스화될 때에만 검사됩니다. for<'a> 아래에서도 일부는 여전히 검사되지만, 많은 것을 여전히 건너뜁니다.
이 동작을 둘러싸고 많은 건전성 문제가 있습니다. 참고: #25860, #84591.
다음을 살펴봅시다:
for<'a, 'b> fn(&'a &'b ())
위의 HRB는 완전히 별개인 두 라이프타임이 아니라 'b: 'a(라이프타임 바운드)를 함의합니다. 이는 정상적인 라이프타임 동작이지만, 적격성 검사 중에는 이 바운드가 일반적으로 참임을 증명할 수 없으므로7 이를 건너뜁니다.
자유 타입 별칭
자유 타입 별칭8의 우변은 정의 위치에서 적격한지 완전히 검사되지 않으며, 우변에 있는 상수 제네릭 인자의 타입만 검사됩니다.
이 글을 작성하는 시점에서 다음 자유 타입 별칭은 타입 검사를 통과합니다:
type WorksButShouldNot = Vec<str>;
---
// This should fail! But we skip the RHS of free type aliases
str: Sized // Not generated
T: Sized와 str: Sized가 모두 Vec<T>에 의해 함의되므로 이 코드는 동작하지 않아야 합니다. 자유 타입 별칭의 우변은 사용되기 전까지 적격성 검사를 거치지 않기 때문에 이 코드는 item-wfck를 “통과“합니다. 이 특정한 경우에는 item-wfck가 사용될 때까지 미뤄집니다.
상수 제네릭의 경우, 자유 타입 별칭의 정의 위치에서는 여전히 소량의 적격성 검사를 수행합니다. 이는 where 바운드 같은 것을 건너뛸 때 상수 제네릭 적격성 검사를 특별히 처리하는 현재 방식과 일치합니다.
즉, 다음 코드는 위 예제와 비슷한 형태임에도 불구하고 마땅히 실패해야 하는 대로 실패합니다:
pub struct Consty<const A: bool>;
type Alias = Consty<42>;
---
// This *is* generated as an obligation, so this (correctly) fails.
42: bool // This is generated!
“well-formed”(적격)입니까, “wellformed“입니까?
논리학 문헌과 일관되도록 “wellformed“보다 “well-formed”(적격)를 사용하십시오. 이 용어는 개발 가이드 / 문서의 다른 부분에서 WF로 줄여 쓰기도 합니다.
비공식적 용법
“적격성(well-formedness)“은 형식 구조의 올바름과 관련된 일반적인 표현이므로, 기여자들이 대화 중에 어떤 것을 “적격(well-formed)“이라고 부르더라도 반드시 여기서 다루는 의미를 뜻하는 것은 아닙니다. 이것이 반드시 잘못된 것은 아니지만 주의해야 합니다.
적격성이 아닌 것
적격성 검사는 “매개변수 개수” 검사나 “매개변수 타입” 검사가 아닙니다9. 적격성 검사라는 용어도, item-wfck라는 용어도 매개변수가 2개인 타입에 1개 또는 3개의 타입이 적용되었는지(기본값이 없다고 가정할 때), 또는 상수 제네릭 매개변수에 타입이 적용되었는지와는 관련이 없습니다. 이러한 종류의 문제는 wfck가 아니라 HIR-ty 로워링10 중에 처리됩니다.
적격성은 라이프타임을 검사하거나 검증하지 않으며, 이는 MIR에서 처리됩니다.
Rust 컴파일러에서의 적격성은 논리학에서처럼 “올바른 구문“에 해당하지 않습니다. 이 용어는 수학적 맥락에서 “주어진 규칙 집합을 따른다“는 의미로 두루 쓰여 온 역사가 있습니다. Rust에서 원래의 용법은 원래의 프로젝션과 적격성에 대한 명확화 RFC 같은 곳에서 볼 수 있듯이, 타입에 대한 바운드와 관련하여 “이것은 내부적으로 일관성이 있다“는 의미에 더 가까웠습니다.
-
언어학에서는 “문법적으로 올바름”, 논리학에서는 “구문적으로 올바름“이며, 수학자들의 일상적인 용법에서는 좀 더 일반적인 “이 영역에 대해 정한 규칙을 따른다“로 읽을 수 있습니다. ↩
-
일반적인 의미의 타입 표현식과 하위 표현식을 가리키며, Rust 컴파일러의 특정 구조체나 열거형을 뜻하지 않습니다. 용어집을 참고하십시오. ↩
-
이 페이지의 일부 영역에서는 “항(Terms)“으로 줄여 부릅니다. ↩
-
이것들은 문서에서 의무(Obligations), 요구 사항(Requirements), 또는 제약(Constraints)이라고 불립니다. 타입의 접미사 및 관련 함수의 이름과 일치하므로 “의무(obligations)“라는 용어를 선호합니다. 향후에는 새 솔버의 용어인 “목표(Goal)“로 대체될 수 있습니다. ↩
-
#checking-types-of-const-arguments ↩
-
대신, 이 바운드는 라이프타임이 인스턴스화될 때 “MIR borrowck” 중에 검사됩니다. ↩
-
어떤 것과도 연관되지 않은 타입 별칭, 즉 모듈 수준의
type Alias = Vec<u8>;입니다. ↩ -
Haskell 같은 언어에서 볼 수 있는 “카인드 검사(kind checking)“라고도 합니다. ↩
-
https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir_analysis/hir_ty_lowering/index.html ↩