Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

타이핑/매개변수 환경

타이핑 환경

타입 시스템과 상호 작용할 때는 트레이트 해결 결과에 영향을 줄 수 있는 몇 가지 변수를 고려해야 합니다. 바로 스코프 내의 where 절 집합, 그리고 컴파일러의 어느 단계에서 타입 시스템 연산이 수행되고 있는지입니다(각각 ParamEnv 구조체와 TypingMode 구조체).

타입 시스템 연산을 수행할 환경이 아직 생성되지 않았을 때는 TypingEnv를 사용하여 필요한 모든 외부 컨텍스트를 하나의 타입으로 묶을 수 있습니다.

타입 시스템 연산을 수행할 컨텍스트(예: ObligationCtxt 또는 FnCtxt)가 생성되고 나면 일반적으로 TypingEnv는 어디에도 저장되지 않습니다. 환경 전체의 속성은 TypingMode뿐이고, ParamEnv는 목표(goal)마다 서로 다른 것을 사용할 수 있기 때문입니다.

매개변수 환경

ParamEnv란 무엇입니까

ParamEnv는 스코프 내 where 절의 목록이며, 일반적으로 특정 항목의 where 절에 대응합니다. 일부 절은 명시적으로 작성되지 않고 ConstArgHasType이나 (일부) 암시적 바운드처럼 clauses_of 쿼리에서 암묵적으로 추가됩니다.

대부분의 경우 ParamEnv는 처음에 param_env 쿼리를 통해 생성되며, 이 쿼리는 주어진 항목의 where 절에서 파생된 ParamEnv를 반환합니다. ParamEnv는 특정 항목에서 파생되지 않은 임의의 절 집합으로도 생성할 수 있습니다. 예를 들어 compare_method_clause_entailment에서는 impl의 where 절과 트레이트 정의에 있는 함수의 where 절로 구성된 혼합 ParamEnv를 생성합니다.


다음과 같은 함수가 있다고 합시다.

#![allow(unused)]
fn main() {
// `foo` would have a `ParamEnv` of:
// `[T: Sized, T: Trait, <T as Trait>::Assoc: Clone]`
fn foo<T: Trait>()
where
    <T as Trait>::Assoc: Clone,
{}
}

개념적으로 foo 내부에 있다면(예를 들어 이를 타입 검사하거나 린트하는 경우) 타입 시스템과 상호 작용하는 모든 곳에서 이 ParamEnv를 사용하게 됩니다. 이를 통해 정규화, 제네릭 상수 평가, where 절/목표 증명 등이 T가 크기가 정해져 있다는 점(sized)이나 Trait를 구현한다는 점 등에 의존할 수 있습니다.

좀 더 구체적인 예는 다음과 같습니다.

#![allow(unused)]
fn main() {
// `foo` would have a `ParamEnv` of:
// `[T: Sized, T: Clone]`
fn foo<T: Clone>(a: T) {
    // when typechecking `foo` we require all the where clauses on `requires_clone`
    // to hold in order for it to be legal to call. This means we have to
    // prove `T: Clone`. As we are type checking `foo` we use `foo`'s
    // environment when trying to check that `T: Clone` holds.
    //
    // Trying to prove `T: Clone` with a `ParamEnv` of `[T: Sized, T: Clone]`
    // will trivially succeed as bound we want to prove is in our environment.
    requires_clone(a);
}
}

또는 컴파일되지 않는 예는 다음과 같습니다.

#![allow(unused)]
fn main() {
// `foo2` would have a `ParamEnv` of:
// `[T: Sized]`
fn foo2<T>(a: T) {
    // When typechecking `foo2` we attempt to prove `T: Clone`.
    // As we are type checking `foo2` we use `foo2`'s environment
    // when trying to prove `T: Clone`.
    //
    // Trying to prove `T: Clone` with a `ParamEnv` of `[T: Sized]` will
    // fail as there is nothing in the environment telling the trait solver
    // that `T` implements `Clone` and there exists no user written impl
    // that could apply.
    requires_clone(a);
}
}

ParamEnv 얻기

타입 시스템과 상호 작용할 때 잘못된 ParamEnv를 사용하면 ICE가 발생하거나, 잘못된 형식의 프로그램이 컴파일되거나, 오류가 나지 않아야 할 때 오류가 발생할 수 있습니다. 올바른 매개변수 환경을 사용하도록 컴파일러를 수정하고 그 과정에서 ICE를 고친 PR의 예로 #82159와 #82067을 참고하십시오.

대부분의 경우 ParamEnv가 필요할 때는 이미 스코프 어딘가나 호출 스택의 상위에 존재하므로, 이를 아래로 전달해야 합니다. 기존 ParamEnv를 찾을 수 있는 곳의 일부 목록은 다음과 같습니다.

사용할 수 있는 ParamEnv가 스코프 어딘가에 있는지 확실하지 않다면 #t-compiler/help Zulip 채널에 스레드를 열어 보는 것도 좋습니다. 누군가가 ParamEnv를 어디서 얻을 수 있는지 알려 줄 수 있습니다.

ParamEnv를 수동으로 구성하는 것은 일반적으로 어떤 최상위 분석(예: HIR 타입 검사 또는 빌림 검사)을 시작할 때만 필요합니다. 이러한 경우 다음 세 가지 방법으로 할 수 있습니다.

대부분의 경우 컴파일러는 특정 정의의 일부로서 분석을 수행하므로, param_env 쿼리를 사용하는 것이 ParamEnv를 구성하는 단연 가장 흔한 방법입니다.

ParamEnv::empty로 빈 환경을 생성하는 것은 일반적으로 코드 생성에서(TypingEnv::fully_monomorphized를 통해 간접적으로) 하거나, 제네릭 매개변수를 전혀 만나지 않을 것으로 예상되는 분석의 일부로서(예: 일관성/고아 검사의 여러 부분)만 수행됩니다.

임의의 where 절 집합으로 환경을 생성하는 것은 대개 불필요하며, 필요한 환경이 소스 코드의 실제 항목에 대응하지 않는 경우에만 수행해야 합니다(예: compare_method_clause_entailment).

ParamEnv의 구성 방식

ParamEnv를 생성하는 것은 사용자가 작성한 대로 항목에 정의된 where 절 목록을 그대로 사용하는 것보다 더 복잡합니다. 슈퍼트레이트를 환경에 확장해 넣는 동시에 모든 별칭을 완전히 정규화해야 합니다. 이 로직은 traits::normalize_param_env_or_error가 처리합니다(이름에 정교화에 관한 언급은 전혀 없지만 그렇습니다).

슈퍼트레이트 확장

fn foo<T: Copy>()와 같은 함수가 있을 때, Copy 트레이트는 Clone 슈퍼트레이트를 가지므로 함수 내부에서 T: Clone을 증명할 수 있기를 원합니다. ParamEnv를 구성할 때는 환경에 있는 모든 트레이트 바운드를 살펴보고, 해당 트레이트에서 발견된 모든 슈퍼트레이트에 대해 새로운 where 절을 ParamEnv에 명시적으로 추가합니다.

구체적인 예로 다음 함수를 들 수 있습니다:

#![allow(unused)]
fn main() {
trait Trait: SuperTrait {}
trait SuperTrait: SuperSuperTrait {}

// `bar`'s unelaborated `ParamEnv` would be:
// `[T: Sized, T: Copy, T: Trait]`
fn bar<T: Copy + Trait>(a: T) {
    requires_impl(a);
}

fn requires_impl<T: Clone + SuperSuperTrait>(a: T) {}
}

환경을 확장하지 않는다면 T: Clone이나 T: SuperSuperTrait를 증명할 수 없으므로 requires_impl 호출은 타입 검사에 실패할 것입니다. 실제로는 환경을 확장하므로 bar의 ParamEnv는 실제로 [T: Sized, T: Copy, T: Clone, T: Trait, T: SuperTrait, T: SuperSuperTrait]입니다. 이 덕분에 bar를 타입 검사할 때 T: Clone과 T: SuperSuperTrait를 증명할 수 있습니다.

Clone 트레이트에는 Sized 슈퍼트레이트가 있지만, (util::elaborate로 구현된) 정교화 과정이 where 절의 중복을 제거하므로 환경에 T: Sized 바운드가 두 개(슈퍼트레이트에서 온 것 하나와 암묵적으로 추가된 T: Sized 바운드 하나) 생기지는 않습니다.

이로 인한 부수 효과로, 슈퍼트레이트의 정교화가 실제로 일어나지 않더라도 환경에 있는 기존 where 절도 중복이 제거됩니다. 다음 예를 참고하십시오:

#![allow(unused)]
fn main() {
trait Trait {}
// The unelaborated `ParamEnv` would be:
// `[T: Sized, T: Trait, T: Trait]`
// but after elaboration it would be:
// `[T: Sized, T: Trait]`
fn foo<T: Trait + Trait>() {}
}

차세대 트레이트 솔버도 이 정교화가 이루어져야 합니다.

모든 바운드 정규화

기존 트레이트 솔버에서는 ParamEnv에 저장된 where 절이 완전히 정규화되어 있어야 합니다. 그렇지 않으면 트레이트 솔버가 올바르게 동작하지 않습니다. ParamEnv를 정규화해야 하는 구체적인 예는 다음과 같습니다:

#![allow(unused)]
fn main() {
trait Trait<T> {
    type Assoc;
}

trait Other {
    type Bar;
}

impl<T> Other for T {
    type Bar = u32;
}

// `foo`'s unnormalized `ParamEnv` would be:
// `[T: Sized, U: Sized, U: Trait<T::Bar>]`
fn foo<T, U>(a: U)
where
    U: Trait<<T as Other>::Bar>,
{
    requires_impl(a);
}

fn requires_impl<U: Trait<u32>>(_: U) {}
}

사람은 <T as Other>::Bar가 u32와 같다는 것을 알 수 있으므로, U에 대한 트레이트 바운드가 U: Trait<u32>와 동등하다는 것을 압니다. 그러나 실제로 이 환경에서 기존 솔버로 U: Trait<u32>를 증명하려고 하면, <T as Other>::Bar가 u32와 같다는 것을 판단할 수 없으므로 실패합니다.

이 문제를 우회하기 위해 ParamEnv를 구성한 후 정규화하며, 그 결과 foo의 ParamEnv는 실제로 [T: Sized, U: Sized, U: Trait<u32>]가 됩니다. 이는 이제 트레이트 솔버가 ParamEnv에 있는 U: Trait<u32>를 사용하여 트레이트 바운드 U: Trait<u32>가 성립한다고 판단할 수 있다는 뜻입니다.

연관 타입을 정규화하려면 ParamEnv가 필요하여 부트스트래핑 문제가 생기므로, 이 우회 방법이 모든 경우에 통하지는 않습니다. 정규화가 올바른 결과를 내려면 정규화된 ParamEnv가 필요하지만, 그 ParamEnv를 얻으려면 정규화를 해야 합니다. 현재는 정규화되지 않은 매개변수 환경을 사용하여 ParamEnv를 한 번 정규화하며, 이 방식이 깨지는 예가 일부 있기는 하지만(예시) 실제로는 대체로 괜찮은 결과를 냅니다.

차세대 트레이트 솔버에는 ParamEnv의 모든 where 절이 완전히 정규화되어야 한다는 요구 사항이 없으므로, ParamEnv를 구성할 때 정규화하지 않습니다.

타이핑 모드

타입 시스템 연산을 어떤 맥락에서 수행하는지에 따라 다른 동작이 필요할 수 있습니다. 예를 들어 일관성(coherence) 검사 중에는 목표(goal)가 성립하지 않는다고 간주할 수 있는 경우나 타입이 서로 같지 않다고 간주할 수 있는 경우에 대해 더 엄격한 요구 사항이 있습니다.

컴파일러의 타입 시스템 연산이 어떤 “단계“에서 수행되고 있는지는 TypingMode 열거형(enum)으로 추적합니다. TypingMode 열거형의 문서가 상당히 잘 작성되어 있으므로, 여기서 그대로 반복하는 대신 API 문서를 직접 읽어 보시기를 권장합니다.