rust-analyzer와 트레이트 솔버 공유하기
rust-analyzer는 컴파일러 프런트엔드로 볼 수 있습니다. rust-analyzer는 파싱, 렉싱, AST 구성 및 로워링, HIR 로워링, 그리고 제한적인 MIR 빌드와 상수 평가까지, rustc에서 코드 생성 이전에 실행되는 부분과 유사한 작업을 수행합니다.
하지만 rust-analyzer는 기본적으로 언어 서버이기 때문에, 그 아키텍처는 몇 가지 중요한 측면에서 rustc의 아키텍처와 다릅니다. 이러한 차이에도 불구하고, rust-analyzer가 맡은 역할 중 상당 부분, 특히 타입 추론과 트레이트 해결은 컴파일러와 겹칩니다.
중복을 피하고 두 구현 간의 일관성을 유지하기 위해, rust-analyzer는 rustc의 여러 크레이트를 재사용하며 가능한 한 공유 추상화에 의존합니다.
공유 크레이트
현재 rust-analyzer는 컴파일러의 다음 rustc_* 크레이트들에 의존합니다.
rustc_abirustc_ast_irrustc_indexrustc_lexerrustc_next_trait_solverrustc_parse_formatrustc_pattern_analysisrustc_type_ir
이 크레이트들은 컴파일러의 일반적인 배포 과정에서 crates.io에 게시되지 않으므로, rust-analyzer는 자체 게시 파이프라인을 유지합니다. rust-analyzer는 rustc-auto-publish 스크립트를 사용하여 이 크레이트들에 ra-ap-rustc_* 접두사를 붙여 crates.io에 게시합니다(예: https://crates.io/crates/ra-ap-rustc_next_trait_solver). 그런 다음 rust-analyzer는 자체 빌드에서 이렇게 다시 게시된 크레이트들에 의존합니다.
특히 트레이트 해결과 관련해서는 rustc_type_ir와 rustc_next_trait_solver가 주요 공유 크레이트이며, 이 크레이트들은 두 컴파일러 프런트엔드가 모두 사용하는 핵심 IR과 솔버 로직을 제공합니다.
추상화 계층
rust-analyzer는 언어 서버이므로 자주 변경되는 소스 코드와 부분적으로 유효하지 않거나 불완전한 소스 코드를 처리해야 합니다. 이를 위해서는 rustc와 상당히 다른 인프라가 필요하며, 특히 소스 코드와 HIR 사이의 계층(예: Ty와 이를 뒷받침하는 인터너)에서 그러합니다.
이러한 차이를 메우기 위해, 컴파일러는 rustc와 rust-analyzer가 공유하는 추상화 계층으로 rustc_type_ir를 제공합니다. 이 크레이트는 타입, 술어, 그리고 트레이트 솔버에 필요한 컨텍스트를 표현하는 데 사용되는 기본 인터페이스를 정의합니다. rustc와 rust-analyzer는 모두 각자의 구체적인 타입 표현에 대해 이 트레이트들을 구현하며, rustc_next_trait_solver는 이러한 추상화에 대해 제네릭하도록 작성되어 있습니다.
이러한 인터페이스 외에도, rustc_type_ir에는 추상화 계층 위에 구축된 몇 가지 복잡한 구성 요소(예: 정교화 로직과 솔버가 사용하는 탐색 그래프 메커니즘)도 포함되어 있습니다.
설계 개념
rustc_next_trait_solver는 rustc_type_ir에 정의된 추상 인터페이스에만 의존하도록 설계되었습니다. 이를 지원하기 위해 rustc_type_ir의 타입 시스템 트레이트는 솔버가 필요로 하는 모든 인터페이스를 노출해야 합니다. 새 추론 타입 변수 생성(rustc, rust-analyzer)이 그 예입니다. 컴파일러별 표현이 필요하지 않은 항목의 경우, rustc_type_ir는 이를 이러한 트레이트로 매개변수화된 구조체나 열거형으로 직접 정의합니다. TraitRef가 그 예입니다.
다음은 rustc_type_ir 크레이트의 주요 항목들입니다.
trait Interner
이 설계의 핵심 트레이트는 Interner이며, rustc와 rust-analyzer 양쪽의 구현별 세부 사항을 모두 지정합니다. 이 트레이트의 핵심 책임은 다음과 같습니다.
- 연관 타입을 통해 구현이 사용하는 구체 타입을 지정합니다. 이 타입들은 각 컴파일러 프런트엔드가 공유 IR을 인스턴스화하는 방식의 근간을 이룹니다.
- 솔버에 필요한 컨텍스트를 제공합니다(예: 언어 항목 쿼리, 트레이트에 대한 모든 블랭킷 impl 열거).
- 그리고 포매팅과 트레이싱을 위해
IrPrint를 구현해야 합니다.
실제로 이러한IrPrintimpl은 rustc나 rust-analyzer 내부의 기존 포매팅 로직으로 연결되기만 합니다.
rustc에서는 TyCtxt가 Interner를 구현합니다. TyCtxt는 rustc의 쿼리 메서드를 노출하며, 필요한 Interner 트레이트 메서드는 해당 쿼리를 호출하는 방식으로 구현됩니다. rust-analyzer에서는 이를 구현하는 타입의 이름이 DbInterner이며(대부분의 인터닝을 salsa 데이터베이스를 통해 수행하기 때문입니다), 그 메서드 대부분은 rustc 쿼리가 아닌 salsa 쿼리를 기반으로 동작합니다.
mod inherent
rustc_type_ir에서 주목할 만한 또 다른 항목은 inherent 모듈입니다. 이 모듈은 Ty나 GenericArg와 같은 컴파일러별 타입에 존재하는 메서드에 대응하는 고유 메서드의 전방 정의를 트레이트 형태로 제공합니다.
이러한 정의 덕분에 제네릭 크레이트(예: rustc_next_trait_solver)는 rustc와 rust-analyzer에서 서로 다르게 구현된 메서드를 호출할 수 있습니다.
제네릭 크레이트의 코드는 다음과 같이 이러한 정의를 임포트해야 합니다.
#![allow(unused)]
fn main() {
use inherent::*;
}
이러한 전방 정의는 구체 구현 자체 내부에서는 절대 사용해서는 안 됩니다. mod inherent의 트레이트를 구현하는 크레이트는 구체 타입의 이름을 지정할 수 있게 되면 해당 타입의 실제 고유 메서드를 호출해야 합니다.
이러한 트레이트에 대한 rustc의 구현은 rustc_middle::ty::inherent 모듈에서 찾을 수 있습니다. rust-analyzer의 경우, 해당 구현은 hir_ty::next_solver::region과 같이 hir_ty::next_solver 아래의 여러 모듈에 흩어져 있습니다.
trait InferCtxtLike와 trait SolverDelegate
이 두 트레이트는 rustc에서 InferCtxt가 맡는 역할에 대응합니다.
InferCtxtLike는 일관성(coherence) 제약(고아 규칙) 때문에 rustc_infer에 정의되어야 합니다. 그 결과, 이 트레이트는 rustc_trait_selection에 있는 기능을 제공할 수 없습니다. 대신 트레이트 해결 로직에 의존하는 동작은 별도의 트레이트인 SolverDelegate로 추상화됩니다. rustc에서 이 트레이트의 구현체는 rustc_trait_selection에 있는 InferCtxt를 감싸는 단순한 newtype 구조체입니다.
(rust-analyzer에서도 자체 InferCtxt를 감싸는 newtype 래퍼에 대해 구현되어 있습니다. 이는 주로 rustc의 구조를 그대로 따르기 위한 것이며, 솔버 관련 로직이 이미 모두 hir-ty 크레이트에 있으므로 엄밀히 말해 필요한 것은 아닙니다.)
장기적으로 이상적인 설계는 현재 SolverDelegate를 통해 표현되는 모든 로직을 rustc_next_trait_solver로 옮기고, 필요한 핵심 연산은 InferCtxtLike에 직접 추가하는 것입니다. 이렇게 하면 솔버 동작의 더 많은 부분이 공유 솔버 크레이트 내부에 온전히 자리 잡을 수 있습니다.
rustc_type_ir::search_graph::{Cx, Delegate}
추상화 트레이트인 Cx와 Delegate는 이미 rustc_next_trait_solver 자체 내에 구현되어 있습니다. 따라서 공유 크레이트의 사용자인 rustc와 rust-analyzer는 별도의 구현을 제공할 필요가 없습니다.
이러한 트레이트는 주로 전체 트레이트 솔버와 독립적으로 탐색 그래프를 퍼징(fuzzing)할 수 있도록 지원하기 위해 존재합니다. 이 인프라는 외부 퍼징 프로젝트인 https://github.com/lcnr/search_graph_fuzz에서 사용됩니다.
파생 트레이트
trait TypeVisitable과TypeVisitable_Generictrait TypeFoldable과TypeFoldable_Generictrait Lift와Lift_Generictrait GenericTypeVisitable
이 트레이트들은 rustc_type_ir에서 많이 사용됩니다. 관련 매크로는 주로 Lift, TypeFoldable, TypeVisitable, GenericTypeVisitable을 구현할 때 매크로가 없으면 작성해야 하는 보일러플레이트를 줄이기 위해 존재합니다.
trait TypeVisitable과 TypeVisitable_Generic
이 트레이트는 구조체나 열거형이 visit_with(...) 메서드를 구현하도록 요구하며, 이 메서드는 다시 TypeVisitor에 제어를 넘깁니다. 이는 여기에서 자세히 확인할 수 있습니다.
TypeVisitable_Generic과 GenericTypeVisitable은 이름 때문에 겉보기에는 비슷해 보이지만, 서로 다른 두 방문 시스템을 구현합니다.
TypeVisitable_Generic은 일반적인TypeVisitable트레이트를Interner에 대해 제네릭하게 derive한다는 뜻입니다.GenericTypeVisitable은 rust-analyzer처럼 nightly가 아닌 환경의 사용자가 쓰는 별도의GenericTypeVisitable트레이트를 derive한다는 뜻입니다.
TypeVisitable_Generic
값의 필드를 선언 순서대로 방문하며, 각 필드를 그 필드 자체의 TypeVisitable<I> 구현에 위임합니다. 방문자가 잔여(residual) 결과를 반환하면 순회가 일찍 중단될 수 있습니다.
필드를 무시하려면 #[type_visitable(ignore)]를 사용하십시오. 해당 필드는 순회에 포함되지 않으며 TypeVisitable<I>를 구현할 필요도 없습니다. 이는 해당 필드를 순회할 필요가 없는 경우에만 사용해야 합니다.
trait TypeFoldable과 TypeFoldable_Generic
이 트레이트는 타입을 내포해야 하는 것들이 구현합니다. 이 개념은 여기에서 자세히 다루며, 소스에서 따라가 볼 수 있습니다.
TypeFoldable_Generic은 구조체나 열거형에 대해 rustc_type_ir::TypeFoldable<I>를 derive합니다.
값을 소비하고, 그 필드를 폴딩한 뒤 동일한 구조체 또는 열거형 배리언트를 재구성합니다. 실패할 수 있는 폴딩 메서드와 실패하지 않는 폴딩 메서드를 모두 생성합니다.
값이 변경 없이 보존되어야 하는 필드에는 #[type_foldable(identity)]를 사용하십시오. 매크로는 해당 필드를 폴더에 전달하지 않고 재구성된 값으로 직접 이동시킵니다. 따라서 해당 필드의 타입은 TypeFoldable<I>를 구현할 필요가 없습니다.
열거형의 경우, 생성된 match에는 variant마다 하나의 재구성 arm이 포함됩니다.
trait Lift와 Lift_Generic
이 트레이트에는 lift_to_interner(...) 메서드가 있습니다. 이름에서 알 수 있듯이, 이 메서드는 무언가를 인터너로 ‘들어 올려야(lift)’ 합니다. 인터너에 대해 더 알아보려면 여기를 참고하시고, 소스는 여기를 참고하십시오.
Lift_Generic 매크로는 구조체나 열거형에 대해 Lift를 derive하며, 쉽게 알아차리기 어려운 고려 사항이 세 가지 있습니다.
- 제네릭 매개변수
I와J는 예약되어 있으며,I는I: Interner를,J는 값을 들어 올릴 대상 인터너를 나타냅니다. PhantomData는 자동으로 처리되어 새PhantomData가 생성되지만,use std::marker::PhantomData;를 통해 파일에 반드시 포함해야 합니다. 구조체의 필드에std::marker::PhantomData를 직접 사용할 수는 없습니다.- 이 바운드는
LiftInto에 대한where절이 아니라Interner트레이트의 연관 타입 바운드로 의도적으로 작성되어 있습니다. 따라서I: LiftInto<J>만 주어져도 Rust는 다음과 같은 바운드를 암시된 것으로 취급할 수 있습니다.
#![allow(unused)]
fn main() {
I::Ty: Lift<J, Lifted = J::Ty>
I::Const: Lift<J, Lifted = J::Const>
}
이를 통해 Lift_Generic은 I: LiftInto<J> 바운드를 내보내면서도 I::Ty, I::Const 및 그 밖에 선언된 연관 타입의 필드에 대해 lift_to_interner를 호출할 수 있습니다. 또한 derive가 I::Assoc를 J::Assoc로 다시 쓴 뒤에 기대되는 대상 필드 타입을 각 호출이 생성하도록 보장합니다.
declare_lift_into!가 없다면 derive는 모든 필드가 사용하는 인터너 연관 타입마다 별도의 바운드를 생성해야 합니다. 새로운 Interner 연관 타입이 Lift_Generic과 함께 동작해야 한다면, 해당 타입에는 적절한 Lift 구현이 필요하며 일반적으로 declare_lift_into! 호출에도 포함되어야 합니다.
리프트할 수 없는 u32 같은 기본 타입 필드처럼 무시하고 싶은 필드가 있다면 #[lift(ignore)]로 해당 필드를 건너뛸 수 있습니다.
trait GenericTypeVisitable
이것은 오로지 rust-analyzer에서만 사용하는, 별도의 더 일반적인 순회 트레이트입니다. 방문자 타입은 메서드의 매개변수가 아니라 트레이트의 매개변수이며, 방문은 결과를 반환하지도 않고 단락(short-circuiting)도 지원하지 않습니다.
따라서 구조체나 열거형은 TypeVisitable_Generic과 GenericTypeVisitable을 모두 derive할 수 있습니다.
ignore 속성은 의도적으로 제공하지 않습니다. 순회는 모든 필드를 방문해야 합니다. 이는 rust-analyzer가 인턴된 타입을 추적하고 가비지 컬렉션할 때 이 순회를 사용하기 위한 건전성 요구 사항입니다.
매크로 크레이트의 nightly 기능이 활성화되면 derive 매크로는 등록된 상태로 남지만 아무 토큰도 내보내지 않습니다. GenericTypeVisitable 트레이트와 그 순회 모듈 역시 rustc_type_ir의 nightly 구성에서는 제외되며, non-nightly 구성에만 존재합니다.
rust-analyzer 지원을 위한 장기 계획
일반적으로 우리는 rustc의 성능이나 유지 보수성을 크게 해치지 않는 한, 이러한 공유 크레이트에서 rust-analyzer를 rustc만큼 잘 지원하는 것을 목표로 합니다. (예: #145377, #146111, #146182, #147723)
rust-analyzer는 stable 툴체인으로 빌드되므로, nightly 전용 기능이 필요한 공유 크레이트는 해당 코드를 nightly 기능 플래그 뒤에 두어야 합니다.
앞으로 우리는 더 많은 공유 로직을 rustc_type_ir로 끌어올릴 계획입니다. rustc와 rust-analyzer 사이에는 ObligationCtxt(rustc, rust-analyzer)와 타입 강제 변환 로직(rustc, rust-analyzer)처럼 여전히 중복된 구현이 있으며, 우리는 이를 점차 통합하고자 합니다.