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

불투명 타입(impl Trait)의 추론

이 페이지에서는 컴파일러가 불투명 타입의 숨겨진 타입을 추론하는 방법을 설명합니다. 이러한 종류의 타입 추론은 다른 종류의 타입 추론과 달리 여러 함수와 함수 본문에 걸쳐 동작할 수 있기 때문에 특히 복잡합니다.

실행 예제

동작 방식을 설명하기 위해 예제 하나를 살펴봅시다.

#![feature(type_alias_impl_trait)]
mod m {
    pub type Seq<T> = impl IntoIterator<Item = T>;

    #[define_opaque(Seq)]
    pub fn produce_singleton<T>(t: T) -> Seq<T> {
        vec![t]
    }

    #[define_opaque(Seq)]
    pub fn produce_doubleton<T>(t: T, u: T) -> Seq<T> {
        vec![t, u]
    }
}

fn is_send<T: Send>(_: &T) {}

pub fn main() {
    let elems = m::produce_singleton(22);

    is_send(&elems);

    for elem in elems {
        println!("elem = {:?}", elem);
    }
}

이 코드에서 불투명 타입은 Seq<T>입니다. 이 타입의 정의 스코프는 모듈 m입니다. 이 타입의 숨겨진 타입은 Vec<T>이며, 이는 m::produce_singleton과 m::produce_doubleton으로부터 추론됩니다.

main 함수에서는 불투명 타입이 정의 스코프 밖에 있습니다. main이 m::produce_singleton을 호출하면 불투명 타입 Seq<i32>에 대한 참조를 돌려받습니다. is_send 호출은 Seq<i32>: Send인지 검사합니다. Send는 impl 트레이트의 바운드 목록에 없지만, 자동 트레이트 누출(auto-trait leakage) 덕분에 이것이 성립함을 추론할 수 있습니다. for 루프의 디슈가링(desugaring)은 Seq<T>: IntoIterator를 요구하며, 이는 Seq<T>에 선언된 바운드로부터 증명할 수 있습니다.

main의 타입 검사

먼저 main을 타입 검사할 때 어떤 일이 일어나는지 살펴봅시다. 처음에 produce_singleton을 호출하며, 반환 타입은 불투명 타입 OpaqueTy입니다.

for 루프의 타입 검사

for 루프는 in elems 부분을 IntoIterator::into_iter(elems)로 디슈가링합니다. elems의 타입은 Seq<T>이므로, 타입 검사기는 Seq<T>: IntoIterator 의무(obligation)를 등록합니다. Seq<T>는 해당 트레이트에 대한 바운드를 가진 불투명 타입(impl IntoIterator<Item = T>)이므로 이 의무는 자명하게 충족됩니다. U: Foo where 바운드가 있으면 U가 Foo를 자명하게 충족하는 것과 마찬가지로, 불투명 타입의 바운드도 타입 검사기가 사용할 수 있으며 의무를 이행하는 데 쓰입니다.

for 루프에서 elem의 타입은 <Seq<T> as IntoIterator>::Item, 즉 T로 추론됩니다. 타입 검사기는 어느 시점에서도 숨겨진 타입에 관심을 두지 않습니다.

is_send 호출의 타입 검사

자동 트레이트 바운드를 증명하려 할 때는 먼저 위와 같은 과정을 반복하여 해당 자동 트레이트가 불투명 타입의 바운드 목록에 있는지 확인합니다. 이것이 실패하면 불투명 타입의 숨겨진 타입을 드러내는데, 이는 일반적으로 드러내는 것이 아니라 오직 이 특정 트레이트 바운드를 증명하기 위해서만 이루어집니다. 드러내기는 불투명 타입의 DefId에 대해 type_of 쿼리를 호출하여 수행합니다. 이 쿼리는 내부적으로 정의 함수(들)에 숨겨진 타입을 요청하여 그것을 반환합니다(자세한 내용은 type_of에 관한 절을 참고하십시오).

타입 검사 단계의 흐름도

flowchart TD
    TypeChecking["type checking `main`"]
    subgraph TypeOfSeq["type_of(Seq<T>) query"]
        WalkModuleHir["Walk the HIR for the module `m`\nto find the hidden types from each\nfunction/const/static within"]
        VisitProduceSingleton["visit `produce_singleton`"]
        InterimType["`produce_singleton` hidden type is `Vec<T>`\nkeep searching"]
        VisitProduceDoubleton["visit `produce_doubleton`"]
        CompareType["`produce_doubleton` hidden type is also Vec<T>\nthis matches what we saw before ✅"]
        Done["No more items to look at in scope\nReturn `Vec<T>`"]
    end

    BorrowCheckProduceSingleton["`borrow_check(produce_singleton)`"]
    TypeCheckProduceSingleton["`type_check(produce_singleton)`"]

    BorrowCheckProduceDoubleton["`borrow_check(produce_doubleton)`"]
    TypeCheckProduceDoubleton["`type_check(produce_doubleton)`"]

    Substitute["Substitute `T => u32`,\nyielding `Vec<i32>` as the hidden type"]
    CheckSend["Check that `Vec<i32>: Send` ✅"]

    TypeChecking -- trait code for auto traits --> TypeOfSeq
    TypeOfSeq --> WalkModuleHir
    WalkModuleHir --> VisitProduceSingleton
    VisitProduceSingleton --> BorrowCheckProduceSingleton
    BorrowCheckProduceSingleton --> TypeCheckProduceSingleton
    TypeCheckProduceSingleton --> InterimType
    InterimType --> VisitProduceDoubleton
    VisitProduceDoubleton --> BorrowCheckProduceDoubleton
    BorrowCheckProduceDoubleton --> TypeCheckProduceDoubleton
    TypeCheckProduceDoubleton --> CompareType --> Done
    Done --> Substitute --> CheckSend

type_of 쿼리 내부

type_of 쿼리를 불투명 타입 O에 적용하면 숨겨진 타입을 반환합니다. 그 숨겨진 타입은 O의 정의 스코프 내에 있는 각 제약 함수의 결과를 결합하여 계산됩니다.

flowchart TD
    TypeOf["type_of query"]
    TypeOf -- find_opaque_ty_constraints --> FindOpaqueTyConstraints
    FindOpaqueTyConstraints --> Iterate
    Iterate["Iterate over each item in defining scope"]
    Iterate -- For each item --> TypeCheck
    TypeCheck["Check typeck(I) to see if it constraints O"]
    TypeCheck -- I does not\nconstrain O --> Iterate
    TypeCheck -- I constrains O --> BorrowCheck
    BorrowCheck["Invoke mir_borrowck(I) to get hidden type\nfor O computed by I"]
    BorrowCheck --> PreviousType
    PreviousType["Hidden type from I\nsame as any previous hidden type\nfound so far?"]
    PreviousType -- Yes --> Complete
    PreviousType -- No --> ReportError
    ReportError["Report an error"]
    ReportError --> Complete["Item I complete"]
    Complete --> Iterate

    FindOpaqueTyConstraints -- All constraints found --> Done
    Done["Done"]

불투명 타입을 다른 타입과 관계 짓기

불투명 타입의 숨겨진 타입이 제약되는 중심 지점은 한 곳뿐이며, 바로 handle_opaque_type 함수입니다. 흥미롭게도 이 함수는 타입 두 개를 받으므로 아무 타입 두 개나 전달할 수 있지만, 그중 하나는 불투명 타입이어야 합니다. 순서는 진단에서만 중요합니다.

flowchart TD
    subgraph typecheck["type check comparison routines"]
        equate.rs
        sub.rs
        lub.rs
    end

    typecheck --> TwoSimul

    subgraph handleopaquetype["infcx.handle_opaque_type"]

        TwoSimul["Defining two opaque types simultaneously?"]

        TwoSimul -- Yes --> ReportError["Report error"]

        TwoSimul -- No --> MayDefine -- Yes --> RegisterOpaqueType --> AlreadyHasValue

        MayDefine -- No --> ReportError

        MayDefine["In defining scope OR in query?"]

        AlreadyHasValue["Opaque type X already has\na registered value?"]

        AlreadyHasValue -- No --> Obligations["Register opaque type bounds\nas obligations for hidden type"]

        RegisterOpaqueType["Register opaque type with\nother type as value"]

        AlreadyHasValue -- Yes --> EquateOpaqueTypes["Equate new hidden type\nwith old hidden type"]
    end

쿼리와의 상호작용

쿼리가 불투명 타입을 처리할 때는 자신이 정의 스코프 안에 있는지 판단할 수 없으므로, 그냥 정의 스코프 안에 있다고 가정합니다.

등록된 숨겨진 타입은 QueryResponse 구조체의 opaque_types 필드에 저장됩니다(take_opaque_types_for_query_response 함수가 이를 읽어 냅니다).

query_response_substitution_guess에서 QueryResponse가 주변 infcx로 인스턴스화될 때, (위에서와 같이) handle_opaque_type을 호출하여 각 숨겨진 타입 제약을 변환합니다.

약간 “이상한” 점이 하나 있습니다. 인스턴스화된 불투명 타입에는 순서가 있습니다(한 불투명 타입이 다른 불투명 타입과 비교되어, 숨겨진 타입을 할당받을 불투명 타입 하나를 골라야 하는 경우). “기대되는(expected)” 것으로 간주되는 쪽을 사용합니다. 하지만 실제로는 두 불투명 타입 모두 정의 사용을 가질 수 있습니다. 쿼리 결과가 인스턴스화될 때, 이는 해당 쿼리를 사용하는 컨텍스트에서 다시 평가됩니다. 최종 컨텍스트(함수의 typeck, mir borrowck 또는 wf-check)는 어느 불투명 타입이 실제로 인스턴스화될 수 있는지 알고 있으므로, 이를 올바르게 처리합니다.

MIR 빌림 검사기 내부

MIR 빌림 검사기는 nll_relate를 통해 대상들을 관계 지으며, 영역(region)에만 관심을 둡니다. 어떤 타입 관계든 숨겨진 타입의 바인딩을 유발하므로 빌림 검사기는 타입 검사기와 같은 일을 하지만, 명백히 죽은 코드(예: 패닉 이후)는 무시합니다. 또한 숨겨진 타입에 관해서는 빌림 검사기가 진실의 원천(source of truth)입니다. 숨겨진 타입의 어떤 라이프타임이 불투명 타입 선언의 어떤 라이프타임에 대응하는지 제대로 알아낼 수 있는 것은 빌림 검사기뿐이기 때문입니다.

하위 호환성을 위한 편법

반환 위치의 impl Trait에는 어떤 RFC에도 포함되지 않았고 의도치 않게 안정화되었을 가능성이 높은 여러 특이한 동작이 있습니다. 이를 지원하기 위해 replace_opaque_types_with_inference_vars를 사용하여 이전 동작을 다시 도입합니다.

하위 호환성을 위한 편법은 세 가지입니다.

  1. 모든 반환 지점은 같은 추론 변수를 공유하므로, 일부 반환 지점은 다른 반환 지점이 구체 타입을 사용하는 경우에만 컴파일될 수 있습니다.

    #![allow(unused)]
    fn main() {
    fn foo() -> impl Debug {
        if false {
            return std::iter::empty().collect();
        }
        vec![42]
    }
    }
  2. impl Trait에 대한 연관 타입 동등성 제약은 숨겨진 타입이 연관 타입의 트레이트 바운드를 만족하는 한 사용할 수 있습니다. 불투명한 impl Trait 시그니처 자체는 이를 만족하지 않아도 됩니다.

    #![allow(unused)]
    fn main() {
    trait Duh {}
    
    impl Duh for i32 {}
    
    trait Trait {
        type Assoc: Duh;
    }
    
    // the fact that `R` is the `::Output` projection on `F` causes
    // an intermediate inference var to be generated which is then later
    // compared against the actually found `Assoc` type.
    impl<R: Duh, F: FnMut() -> R> Trait for F {
        type Assoc = R;
    }
    
    // The `impl Send` here is then later compared against the inference var
    // created, causing the inference var to be set to `impl Send` instead of
    // the hidden type. We already have obligations registered on the inference
    // var to make it uphold the `: Duh` bound on `Trait::Assoc`. The opaque
    // type does not implement `Duh`, even if its hidden type does.
    // Lazy TAIT would error out, but we inserted a hack to make it work again,
    // keeping backwards compatibility.
    fn foo() -> impl Trait<Assoc = impl Send> {
        || 42
    }
    }
  3. 클로저는 부모 함수의 impl Trait에 대한 숨겨진 타입을 만들 수 없습니다. 1번 항목이 추론 변수를 도입하기 때문에 클로저는 언제나 추론 변수만 보게 되므로 이 점은 대체로 문제가 되지 않지만, 1번을 고친다면 이는 문제가 될 것입니다.