캐싱과 그에 따른 미묘한 고려 사항
일반적으로 우리는 트레이트 선택의 결과를 캐시하려고 합니다. 이는 다소 복잡한 과정입니다. 그 이유 중 하나는 트레이트 참조에 있는 타입이 모두 완전히 알려지지 않은 경우에도 결과를 캐시할 수 있기를 원하기 때문입니다. 그런 경우 트레이트 선택 과정이 타입 변수에도 영향을 줄 수 있으므로, 선택 과정의 결과를 캐시할 수 있어야 할 뿐만 아니라 타입 변수에 미친 영향도 재현할 수 있어야 합니다.
예시는 다음과 같습니다.
캐시 동작 방식의 기본 개념은 먼저 바인딩되지 않은 모든 추론 변수를 플레이스홀더 버전으로 대체하는 것입니다. 따라서 $t가 바인딩되지 않은 추론 변수인 트레이트 참조 usize : Foo<$t>가 있다면, 이를 usize : Foo<$0>로 대체할 수 있습니다. 여기서 $0은 플레이스홀더 타입입니다. 그런 다음 캐시에서 이를 조회합니다.
캐시 적중이 발생하면, 선택 과정에서 바로 다음에 취할 단계를 그 적중이 알려 줍니다(예: impl #22를 적용하거나 where 절 X : Foo<Y>를 적용).
반면 적중이 없으면 선택 과정을 처음부터 거쳐야 합니다. 가능한 유일한 impl이 def-id가 22인 다음 impl이라는 결론에 도달했다고 가정해 봅시다:
impl Foo<isize> for usize { ... } // Impl #22
그러면 캐시에 usize : Foo<$0> => ImplCandidate(22)를 기록합니다. 다음으로 ImplCandidate(22)를 확정하며, 이는 (부수 효과로) $t를 isize와 단일화합니다.
이제 나중에 어느 시점에서 usize : Foo<$u>를 마주칠 수 있습니다. 이를 플레이스홀더로 대체하면 앞에서와 마찬가지로 usize : Foo<$0>가 되므로, 캐시 조회가 성공하여 ImplCandidate(22)를 얻습니다. ImplCandidate(22)를 확정하면 (부수 효과로) $u가 isize와 단일화됩니다.
where 절과 로컬 캐시 대 전역 캐시
한 가지 미묘한 상호 작용은 트레이트 조회 결과가 스코프 안에 어떤 where 절이 있는지에 따라 달라진다는 점입니다. 그래서 실제로는 로컬 캐시와 전역 캐시, 이렇게 두 개의 캐시가 있습니다. 로컬 캐시는 ParamEnv에 연결되어 있고, 전역 캐시는 tcx에 연결되어 있습니다. 결과가 스코프 안의 where 절에 의존할 수 있는 경우에는 언제나 로컬 캐시를 사용합니다. 어떤 캐시를 사용할지는 select.rs에 있는 pick_candidate_cache 메서드가 결정합니다. 현재는 매우 단순하고 보수적인 규칙을 사용합니다. 스코프 안에 where 절이 하나라도 있으면 로컬 캐시를 사용합니다. 예전에는 더 세밀하게 구분하려고 시도했지만, 그 때문에 #22019와 #18290 같은 성가시고 이상한 버그가 잇따라 발생했습니다. 이 단순한 규칙은 꽤 확실하게 안전해 보이며, 여전히 매우 높은 적중률(rustc를 컴파일할 때 약 95%)을 유지합니다.
TODO: pick_candidate_cache는 더 이상 존재하지 않는 것으로 보입니다. 전반적으로 이 절의 내용이 아직도 정확하기는 합니까?