강제 변환
강제 변환은 값을 다른 타입으로 바꾸는 암시적 연산입니다. 강제 변환 지점은 강제 변환이 암묵적으로 수행될 수 있는 위치입니다. 강제 변환 지점에는 두 가지 종류가 있습니다:
- 일대일
- LUB(최소 상계, Least-Upper-Bound)
#![allow(unused)]
fn main() {
let one_to_one_coercion: &u32 = &mut 8;
let lub_coercion = match my_bool {
true => &mut 10,
false => &12,
};
}
어떤 강제 변환이 있으며 어떤 표현식이 강제 변환 지점인지에 대한 설명은 강제 변환에 관한 레퍼런스 페이지를 참조하십시오: https://doc.rust-lang.org/reference/type-coercions.html
일대일 강제 변환
일대일 강제 변환에서는 하나의 단일 타입을 알려진 대상 타입으로 강제 변환합니다. 위 예제에서는 &mut u32에서 &u32로의 강제 변환이 이에 해당합니다.
일대일 강제 변환은 FnCtxt::coerce를 호출하여 수행할 수 있습니다.
LUB 강제 변환
LUB 강제 변환에서는 소스 타입들의 집합을 알려지지 않은 어떤 대상 타입으로 강제 변환합니다. 일대일 강제 변환과 달리, LUB 강제 변환은 모든 소스 타입이 강제 변환되는 대상 타입을 만들어 냅니다.
위 예제에서는 &mut i32와 &i32 둘 모두에 대한 LUB 강제 변환이 이에 해당하며, 여기서 대상 타입 &i32가 만들어집니다.
“LUB 강제 변환”(최소 상계 강제 변환)이라는 이름은 이 강제 변환이 타입들의 집합을 받아, 두 소스 타입이 모두 강제 변환/서브타입화될 수 있는 가장 작은 강제 변환/서브타입 타입을 계산하는 방식에서 유래했습니다.
LUB 강제 변환을 수행하는 일반적인 과정은 다음과 같습니다:
// * 1
let mut coerce = CoerceMany::new(initial_lub_ty);
for expr in exprs {
// * 2
let expr_ty = fcx.check_expr_with_expectation(expr, expectation);
coerce.coerce(fcx, &cause, expr, expr_ty);
}
// * 3
let final_ty = coerce.complete(fcx);
여기에는 몇 가지 핵심 단계가 있습니다:
CoerceMany값을 생성하고 초기 lub를 선택하기- 각 표현식을 타입 검사하고 그 타입을 LUB 강제 변환의 일부로 등록하기
- LUB 강제 변환을 완료하여 결과로 나오는 lub 타입을 얻기
1단계
먼저 CoerceMany 값을 생성합니다. 이 값은 LUB 강제 변환에 필요한 모든 상태를 저장합니다. 일대일 강제 변환과 달리, LUB 강제 변환은 단일 함수 호출이 아닙니다. 타입 검사와 LUB 강제 변환의 진행을 번갈아 수행하고자 하기 때문입니다.
CoerceMany를 생성하려면 어떤 initial_lub 타입이 필요합니다. 이는 강제 변환의 대상과는 다릅니다. 대상은 (일대일 강제 변환과 달리) LUB 강제 변환의 입력이 아니라 출력입니다.
초기 lub 타입은 이 LUB 강제 변환이 적용되는 표현식의 Expectation으로부터 도출되어야 합니다. 이를 통해 LUB 강제 변환을 계산하면서 생긴 추론 제약이, 해당 LUB 강제 변환에 참여하는 이후 표현식들을 타입 검사하는 데 쓰이는 Expectation들로 전파될 수 있습니다.
이것이 미치는 영향에 대한 자세한 내용은 “불필요한 추론 제약” 절을 참조하십시오.
사용할 Expectation이 없다면 초기 lub 타입으로 쓸 새로운 추론 변수를 만들어야 합니다.
2단계
다음으로, LUB 강제 변환에 참여하는 각 표현식의 타입을 검사한 뒤 그 타입으로 CoerceMany::coerce를 호출합니다.
LUB 강제 변환에 참여하는 표현식이 실제로는 HIR에 존재하지 않는 경우도 있습니다. 예를 들어 피연산자가 없는 break 또는 return 표현식을 처리할 때는 ()가 LUB 강제 변환에 참여해야 합니다.
이러한 경우에는 CoerceMany::coerce_forced_unit 메서드를 사용할 수 있습니다.
CoerceMany::coerce 메서드와 coerce_forced_unit 메서드는 모두 새 타입 때문에 LUB 강제 변환을 만족할 수 없게 되면 오류를 발생시킵니다. 이 경우 LUB 강제 변환의 최종 타입은 오류 타입이 됩니다.
3단계
마지막으로 모든 표현식의 강제 변환이 끝나면 CoerceMany::complete를 호출하여 LUB 강제 변환의 최종 타입을 얻을 수 있습니다.
LUB 강제 변환의 결과 타입은 CoerceMany를 생성할 때 전달한 초기 lub 타입과 의미 있게 다를 수 있습니다. 항상 LUB 강제 변환의 결과 타입을 가져와 필요한 검사를 수행해야 합니다.
구현상의 세부 사항
조정
강제 변환 연산이 성공하면 그것이 어떤 종류의 강제 변환이었는지(예: unsize 강제 변환, 자동 역참조 등)를 기록합니다. 이는 강제 변환 연산의 일부로 처리되며, 진행 중인 TypeckResults에 조정(adjustment) 목록을 기록하는 방식으로 이루어집니다.
THIR을 구축할 때는 TypeckResults에 저장된 조정 정보를 가져와 모든 강제 변환 단계를 명시적으로 만듭니다. 컴파일러에서 이 시점 이후로는 강제 변환이라는 개념이 사실상 존재하지 않으며, MIR에는 명시적 캐스트와 서브타이핑만 남습니다.
TODO: 여기에 조정 장을 작성하고 링크하십시오
CoerceMany의 동작 방식
CoerceMany는 현재 lub 타입과 새로운 원본 타입을 반복해서 받아, 두 타입이 모두 강제 변환될 수 있는 새로운 lub 타입을 계산하는 방식으로 동작합니다. 두 타입을 받아 새로운 세 번째 타입을 계산하는 핵심 로직은 try_find_coercion_lub에서 찾을 수 있습니다.
#![allow(unused)]
fn main() {
fn foo() {}
fn bar() {}
let a = match my_bool {
true => foo,
true if other_bool => foo,
false => bar,
}
}
이 예제에서는 match 표현식을 타입 검사할 때 LUB 강제 변환이 수행됩니다. let 문에 알려진 타입이 없으므로, 이 LUB 강제 변환은 어떤 추론 변수 ?x를 초기 lub 타입으로 삼아 시작합니다.
이 LUB 강제 변환에 참여하는 표현식은 세 개입니다. LUB 강제 변환의 첫 번째 표현식은 특별합니다. 기존 초기 lub 타입과 함께 새 타입을 계산하는 대신, 첫 번째 표현식에서 초기 lub 타입으로 직접 강제 변환합니다.
true => foo,를 타입 검사하고 나면FnDef(Foo)타입을 얻습니다. 그런 다음CoerceMany::coerce를 호출하며, 이 메서드는FnDef(Foo)에서?x로의 일대일 강제 변환을 수행합니다. 이를 통해?x=FnDef(Foo)가 추론되고, LUB 강제 변환의 새로운 lub 타입을 얻게 됩니다.true if other_bool => foo,를 타입 검사하고 나면 다시 한번FnDef(Foo)타입을 얻습니다. 그런 다음CoerceMany::coerce를 호출하며, 이 메서드는 이전 lub 타입(FnDef(Foo))과 이 표현식의 타입(FnDef(Foo))으로부터 새로운 lub 타입을 계산하려고 시도합니다. 그 결과 lub 타입은FnDef(Foo)가 됩니다.false => bar,를 타입 검사하고 나면FnDef(Bar)타입을 얻습니다. 그런 다음CoerceMany::coerce를 호출하며, 이 메서드는 이전 lub 타입(FnDef(Foo))과 이 표현식의 타입(FnDef(Bar))으로부터 새로운 lub 타입을 계산하려고 시도합니다. 이 경우에는 두 함수 항목 타입을 모두 함수 포인터로 강제 변환하기로 하므로fn() -> ()타입을 얻게 됩니다.
이렇게 하여 fn() -> ()의 LUB 강제 변환에 대한 최종 타입을 얻습니다.
전이적 강제 변환
현재 대상 타입을 새 타입으로 반복해서 강제 변환하려고 시도하는 CoerceMany의 알고리즘은 현재 “전이적 강제 변환(Transitive Coercions)“을 초래합니다. LUB 강제 변환의 한 단계에서 어떤 표현식을 강제 변환한 뒤, 이후 단계에서 그 표현식을 다시 강제 변환할 수 있습니다.
struct Foo;
use std::ops::Deref;
impl Deref for Foo {
type Target = [u8; 2];
fn deref(&self) -> &[u8; 2] {
&[1; _]
}
}
fn main() {
match () {
_ if true => &Foo,
_ if true => &[1_u8; 2],
_ => &[1_u8; 2] as &[u8],
};
}
여기서는 초기 lub 타입이 ?x인 LUB 강제 변환이 있습니다. 첫 번째 단계에서는 &Foo에서 ?x로의 일대일 강제 변환을 수행합니다(첫 번째 단계는 특별하다는 점을 상기하십시오).
두 번째 단계에서는 현재 lub 타입인 &Foo와 새 타입인 &[u8; 2]로부터 새로운 lub 타입을 계산합니다. 첫 번째 표현식에 &Foo에서 &[u8; 2]로의 역참조 강제 변환을 수행하면 이 새로운 lub 타입은 &[u8; 2]가 됩니다.
세 번째 단계에서는 현재 lub 타입인 &[u8; 2]와 새 타입인 &[u8]로부터 새로운 lub 타입을 계산합니다. 처음 두 표현식에 &[u8; 2]에서 &[u8]로의 크기 제거 강제 변환을 수행하면 이 새로운 lub 타입은 &[u8]가 됩니다.
첫 번째 표현식이 두 번 강제 변환된다는 점에 주목하십시오. 한 번은 &Foo에서 &[u8; 2]로의 역참조 강제 변환이고, 그다음은 &[u8; 2]에서 &[u8]로의 크기 제거 강제 변환입니다.
전이적 강제 변환의 현재 구현은 망가져 있으며, 앞의 예제는 실제로 stable에서 ICE를 일으킵니다. LUB 강제 변환을 수행하는 로직은 전이적 강제 변환을 문제없이 만들어 낼 수 있지만, 컴파일러의 나머지 부분은 이를 처리하도록 갖추어져 있지 않습니다.
또한 일대일 강제 변환은 LUB 강제 변환이 만들어 낼 수 있는 전이적 강제 변환 중 상당수를 만들어 낼 수 없습니다. 예를 들어, 앞의 예제를 일대일 강제 변환으로 바꾸면 다음과 같은 컴파일 오류가 발생합니다:
struct Foo;
use std::ops::Deref;
impl Deref for Foo {
type Target = [u8; 2];
fn deref(&self) -> &[u8; 2] {
&[1; _]
}
}
fn main() {
let a: &[u8] = &Foo;
}
여기서는 &Foo에서 &[u8]로의 일대일 강제 변환을 시도하지만, 역참조 강제 변환 또는 크기 제거 강제 변환 중 하나만 수행할 수 있고 둘을 조합할 수는 없으므로 실패합니다.
try_find_coercion_lub는 어떻게 동작합니까
LUB 강제 변환에서 새로운 lub 타입을 계산하는 방법은 세 가지입니다.
- 현재 lub 타입과 새 타입을 모두 함수 포인터로 강제 변환합니다
- 현재 lub 타입을 새 타입으로 강제 변환합니다(또는 그 반대)
- 현재 lub 타입과 새 타입의 공통 상위 타입을 계산합니다
안타깝게도 실제 구현에서는 이 구조가 상당히 불분명하게 드러납니다.
공통 상위 타입 계산은 일대일 강제 변환 로직을 재사용하기 때문에 암묵적으로 일어나는데, 이 로직은 강제 변환이 실패하면 이미 서브타이핑을 처리합니다.
또한 현재 lub 타입과 새 타입을 모두 함수 포인터로 강제 변환하려고 할 때, 불필요한 강제 변환을 피하기 위해 공통 상위 타입을 먼저 계산하려고 시도합니다.
이 함수의 구조를 개념적 모델에 더 가깝게 맞추도록 개선할 여지가 있을 것입니다.
일대일 강제 변환의 use_lub 필드
일대일 강제 변환의 구현은 LUB 강제 변환의 일부로 재사용됩니다.
LUB 강제 변환이 시그니처를 관계 지을 때 단방향 서브타이핑을 사용하거나, 가능한 강제 변환이 없을 때 서브타이핑으로 대체하는 것은 잘못된 일입니다. 대신 두 타입의 공통 상위 타입을 계산해야 합니다.
Coerce의 use_lub 필드는 일반적인 서브타이핑을 수행할지(일대일 강제 변환의 경우), 아니면 공통 상위 타입을 계산할지(LUB 강제 변환의 경우)를 전환하기 위해 존재합니다.
Lub 계산
이론적으로 공통 상위 타입을 계산하는 일은 새 추론 변수 ?mutual_sup를 만든 다음 lub_ty <: ?mutual_sup와 new_ty <: ?mutual_sup를 요구하는 것만큼 간단해야 합니다. 실제로는 LUB 강제 변환이 특별한 TypeRelation인 LatticeOp를 사용합니다.
이는 주로 고차 랭크 타입의 서브타이핑/일반화가 상당히 망가져 있는 문제를 우회하기 위한 것입니다. 일반적인 서브타이핑과 달리, lub 타입 관계는 고차 랭크 타입을 만나면 불변성으로 전환됩니다.
이렇게 하면 고차 랭크 타입의 바인더들이 동등하도록 강제되므로, “가장 일반적인” 바인더를 고를 필요가 없어집니다. 그런 바인더를 고르기는 상당히 어렵습니다.
또한 공통 상위 타입을 계산하는 과정이 순서에 의존하지 않도록 합니다. 타입 a와 b가 주어졌을 때, a와 b의 공통 상위 타입을 계산한 결과가 b와 a의 공통 상위 타입을 계산한 결과와 같다면 바람직할 것입니다.
공통 상위 타입을 계산하는 단순한 방법을 사용한다면, 고차 타입과 서브타이핑에 관한 현재의 문제들 때문에 이 성질이 성립하지 않게 됩니다.
MIR 빌드 중에 강제 변환이 명시적인 MIR 연산으로 바뀌므로, LUB 강제 변환의 최종 타입을 계산하는 과정은 HIR 타입 검사 중에만 일어납니다. 이는 또한 공통 상위 타입을 계산하는 동작이 타입 추론에만 영향을 미치며 건전성과는 관련이 없다는 것을 의미합니다.
주의 사항
프로브
일대일 강제 변환과 LUB 강제 변환 모두 프로브로 되돌릴 수 없는 부수 효과를 가지므로, 프로브 내부에서 강제 변환을 수행할 때는 주의해야 합니다.
LUB 강제 변환은 강제 변환 단계가 실패하면 오류를 발생시키며, 이 때문에 프로브 내부에서 사용하기에 전적으로 적합합니다.
일대일 강제 변환과 LUB 강제 변환은 모두 성공 시 강제 변환된 표현식에 조정을 적용합니다. 따라서 프로브 내부에서 강제 변환 시도가 성공했다면, 그 프로브는 아무것도 롤백해서는 안 됩니다.
따라서 FnCtxt::coerce 호출을 commit_if_ok 안에 감싸는 것은 올바르지만, coerce 호출 이후에 Err를 반환한다면 그렇게 하는 것은 잘못입니다. probe 내부에서 FnCtxt::coerce를 호출하는 것 또한 잘못입니다.
CoerceMany는 probe나 commit_if_ok 내부에서 절대 사용해서는 안 됩니다.
Never-to-Any 강제 변환
never 타입(!)에서 추론 변수로 강제 변환하면, 그 추론 변수를 대상 타입으로 하는 NeverToAny 강제 변환이 만들어집니다. 이는 추론 변수를 never 타입과 단일화하는 것과 미묘하게 다릅니다.
어떤 추론 변수 ?x를 !와 단일화하려면 ?x가 실제로 !와 같아야 합니다. 그러나 NeverToAny 강제 변환에서는 ?x가 가능한 어떤 타입으로든 추론될 수 있습니다.
이러한 차이 때문에, 강제 변환의 초기 lub 타입이 추론 변수인 경우(예: 초기 lub 타입으로 사용할 Expectation이 없는 경우)에도 서브타이핑 대신 강제 변환을 사용하는 것이 여전히 중요합니다.
강제 변환을 거치지 않고 never 타입으로 잘못 추론하던 버그를 수정한 PR #147834를 참고하십시오.
서브타이핑으로의 폴백
서브타이핑은 강제 변환이 아니지만, FnCtxt::coerce와 CoerceMany::coerce/coerce_forced_unit는 모두 서브타이핑 덕분에 성공할 수 있습니다.
일대일 강제 변환에서는 원본 타입이 대상 타입의 서브타입이 되도록 강제하려고 시도합니다. LUB 강제 변환의 경우, 기존의 모든 타입의 상위 타입이 되는 타입을 계산하려고 시도합니다.
예를 들어 ?x를 u32로 일대일 강제 변환하면 서브타이핑으로 폴백하여 ?x eq u32로 추론합니다. 따라서 강제 변환이 실패했다면 그 후에 서브타이핑을 시도할 필요가 없습니다.
불필요한 추론 제약
Expectation의 타입을 초기 lub 타입으로 사용하면, 추론 변수가 LUB 강제 변환에 참여하는 표현식들의 타입에 의해 제약될 수 있습니다. 이러한 추론 변수는 실제로는 LUB 강제 변환의 최종 타입에 의해서만 제약되면 되므로, 이는 항상 바람직하지는 않습니다.
#![allow(unused)]
fn main() {
fn foo<T>(_: T) {}
fn a() {}
fn b() {}
foo::<?x>(match my_bool {
true => a,
false => b,
})
}
여기에는 첫 번째 표현식의 타입이 FnDef(a)이고 두 번째 표현식의 타입이 FnDef(b)인 LUB 강제 변환이 있습니다. ?x를 LUB 강제 변환의 초기 lub 타입으로 사용하면 다음과 같이 동작하게 됩니다.
- 표현식 1:
?x=FnDef(a)로 추론합니다. - 표현식 2:
FnDef(a), FnDef(b)사이의 강제 변환 lub를 찾으며, 그 결과는fn() -> ()입니다. - LUB 강제 변환의 최종 타입은
fn() -> ()입니다.?x eq fn() -> ()로 동일시하는데,?x는 이미FnDef(a)로 추론되었으므로 이는 사실상 성립하지 않는FnDef(a) eq fn() -> ()를 동일시하는 것입니다.
이러한 바람직하지 않은 추론 제약 중 일부(전부는 아닙니다)를 피하기 위해, LUB 강제 변환에 대한 Expectation이 추론 변수라면 이를 초기 lub 타입으로 사용하지 않습니다. 대신 새로운 추론 변수를 생성합니다. 예를 들어 위 코드 조각에서는 ?x를 사용하는 대신 초기 lub 타입으로 새로운 추론 변수 ?y를 만들게 됩니다.
- 표현식 1:
?y=FnDef(a)로 추론합니다. - 표현식 2:
FnDef(a), FnDef(b)사이의 강제 변환 lub를 찾으며, 그 결과는fn() -> ()입니다. - LUB 강제 변환의 최종 타입은
fn() -> ()이며,?x=fn() -> ()로 추론합니다.
새로운 추론 변수를 생성하지 않아 바람직하지 않은 추론 제약이 발생했던 사례는 #140283을 참고하십시오.
이 방법으로 모든 경우의 불필요한 제약을 피할 수 있는 것은 아니며, Expectation이 추론 변수인 가장 흔한 경우만 피할 수 있습니다. 이론적으로는 모든 경우에 이러한 제약을 피하는 것이 바람직하겠지만, 그렇게 하려면 상당히 복잡한 작업이 필요합니다.