컴파일러 소스의 고수준 개요
컴파일러가 무엇을 하는지 살펴보았으니, 이제 rustc 소스 코드가 있는 rust-lang/rust 저장소의 구조를 살펴봅시다.
이 장을 읽기 전에 컴파일러의 동작 방식을 소개하는 “컴파일러 개요” 장을 먼저 읽으면 도움이 될 수 있습니다.
워크스페이스 구조
rust-lang/rust 저장소는 컴파일러, 표준 라이브러리(core, alloc, std, proc_macro, etc), rustdoc뿐 아니라 빌드 시스템과 완전한 Rust 배포판을 빌드하기 위한 여러 도구 및 서브모듈을 포함하는 하나의 커다란 cargo 워크스페이스로 구성되어 있습니다.
저장소는 세 개의 주요 디렉터리로 구성되어 있습니다.
-
compiler/에는rustc의 소스 코드가 들어 있습니다. 이 디렉터리는 함께 컴파일러를 이루는 여러 크레이트로 구성되어 있습니다. -
library/에는 표준 라이브러리(core,alloc,std,proc_macro,test)와 Rust 런타임(backtrace,rtstartup,lang_start)이 들어 있습니다. -
tests/에는 컴파일러 테스트가 들어 있습니다. -
src/에는rustdoc,clippy,cargo, 빌드 시스템, 언어 문서 등의 소스 코드가 들어 있습니다.
컴파일러
컴파일러는 여러 compiler/ 크레이트에 구현되어 있습니다. compiler/ 크레이트의 이름은 모두 rustc_*로 시작합니다. 이들은 아주 작은 것부터 매우 큰 것까지 크기가 다양한, 서로 의존하는 약 50개의 크레이트 모음입니다. 실제 바이너리(즉, main 함수)인 rustc 크레이트도 있습니다. 이 크레이트는 다른 크레이트에 있는 컴파일의 여러 부분을 구동하는 rustc_driver 크레이트를 호출하는 것 외에는 실제로 아무 일도 하지 않습니다.
이 크레이트들의 의존성 순서는 복잡하지만, 대략 다음과 같습니다.
rustc(바이너리)는rustc_driver::main을 호출합니다.rustc_driver는 다른 많은 크레이트에 의존하지만, 그중 주된 것은rustc_interface입니다.rustc_interface는 다른 컴파일러 크레이트 대부분에 의존합니다. 이것은 전체 컴파일 과정을 구동하기 위한 상당히 제네릭한 인터페이스입니다.- 다른
rustc_*크레이트 대부분은rustc_middle에 의존하며, 이 크레이트는 컴파일러의 핵심 자료 구조를 다수 정의합니다. rustc_middle과 다른 크레이트 대부분은 컴파일러의 초기 부분(예: 파서), 기본 자료 구조(예:Span), 또는 오류 보고를 담당하는 소수의 크레이트에 의존합니다.rustc_data_structures,rustc_span,rustc_errors등이 이에 해당합니다.
다른 Rust 패키지와 마찬가지로 cargo tree를 실행하면 정확한 의존성을 확인할 수 있습니다.
cargo tree --package rustc_driver
마지막으로, src/llvm-project는 우리가 포크한 LLVM의 서브모듈입니다. 부트스트래핑 중에 LLVM이 빌드되며, compiler/rustc_llvm 크레이트에는 (C++로 작성된) LLVM을 감싸는 Rust 래퍼가 들어 있어 컴파일러가 LLVM과 상호 작용할 수 있습니다.
이 책의 대부분은 컴파일러에 관한 내용이므로, 여기서는 이 크레이트들에 대해 더 설명하지 않겠습니다.
큰 그림
컴파일러의 의존성 구조는 두 가지 주요 요인의 영향을 받습니다.
- 구성. 컴파일러는 거대한 코드베이스이므로, 하나의 크레이트로 만든다면 감당할 수 없을 만큼 커질 것입니다. 의존성 구조는 부분적으로 컴파일러의 코드 구조를 반영합니다.
- 컴파일 시간. 컴파일러를 여러 크레이트로 나누면 cargo를 사용한 증분/병렬 컴파일의 이점을 더 잘 활용할 수 있습니다. 특히 크레이트 하나를 변경했을 때 다시 빌드해야 하는 크레이트 수를 줄이기 위해 크레이트 간 의존성을 가능한 한 적게 유지하려고 합니다.
의존성 트리의 가장 아래에는 컴파일러 전체에서 사용하는 몇 개의 크레이트가 있습니다(예: rustc_span). 컴파일 과정의 가장 초기 단계(예: 파싱과 추상 구문 트리(AST))는 이 크레이트들에만 의존합니다.
AST가 구성되고 다른 초기 분석이 끝나면 컴파일러의 쿼리 시스템이 설정됩니다. 쿼리 시스템은 함수 포인터를 사용하는 영리한 방식으로 설정됩니다. 이 방식 덕분에 크레이트 간 의존성을 끊을 수 있어 더 많은 병렬 컴파일이 가능합니다. 쿼리 시스템은 rustc_middle에 정의되어 있으므로 컴파일러의 이후 부분은 거의 모두 이 크레이트에 의존합니다. 이 크레이트는 매우 커서 컴파일 시간이 오래 걸립니다. 이 크레이트에서 코드를 분리해 내려는 노력이 몇 차례 있었으며, 성과는 제각각이었습니다. 또 다른 부작용은 관련된 기능이 때때로 여러 크레이트에 흩어진다는 점입니다. 예를 들어 린트 기능은 크레이트의 초기 부분, rustc_lint, rustc_middle 및 기타 여러 곳에 걸쳐 있습니다.
이상적으로는 증분 컴파일과 병렬 컴파일이 컴파일 시간을 적정 수준으로 유지해 주면서, 크레이트 수는 더 적고 각 크레이트의 응집도는 더 높은 것이 바람직합니다. 하지만 증분 컴파일과 병렬 컴파일이 아직 그 정도로 발전하지 못했기 때문에, 지금까지는 여러 크레이트로 나누는 것을 해결책으로 삼아 왔습니다.
의존성 트리의 최상단에는 rustc_driver와, 쿼리 시스템을 감싸 컴파일의 여러 단계를 구동하도록 돕는 불안정한 래퍼인 rustc_interface가 있습니다. 컴파일러를 사용하는 다른 도구는 이 인터페이스를 다른 방식으로 사용할 수 있습니다(예: rustdoc 또는 언젠가는 rust-analyzer). rustc_driver 크레이트는 먼저 명령줄 인자를 파싱한 다음, rustc_interface를 사용하여 컴파일을 끝까지 구동합니다.
rustdoc
rustdoc의 대부분은 librustdoc에 있습니다. 하지만 rustdoc 바이너리 자체는 src/tools/rustdoc이며, 이 바이너리는 rustdoc::main을 호출하는 것 외에는 아무 일도 하지 않습니다.
문서용 JavaScript와 CSS도 src/tools/rustdoc-js와 src/tools/rustdoc-themes에 있습니다. --output-format=json의 타입 정의는 src/rustdoc-json-types에 있는 별도의 크레이트에 있습니다.
rustdoc에 대한 자세한 내용은 이 장을 참고하십시오.
테스트
위의 모든 항목에 대한 테스트 스위트는 tests/에 있습니다. 테스트 스위트에 대한 자세한 내용은 이 장에서 확인하십시오.
테스트 하네스는 src/tools/compiletest/에 있습니다.
빌드 시스템
저장소에는 컴파일러, 표준 라이브러리, rustdoc 등을 빌드하고, 테스트하고, 전체 Rust 배포판을 빌드하는 등의 작업만을 위한 여러 도구가 있습니다.
주요 도구 중 하나는 src/bootstrap/입니다. 부트스트래핑에 대한 자세한 내용은 이 장에서 읽을 수 있습니다. 이 과정에서는 tidy/나 compiletest/처럼 src/tools/에 있는 다른 도구를 사용할 수도 있습니다.
표준 라이브러리
이 코드는 대부분의 다른 Rust 크레이트와 상당히 비슷하지만, 불안정한(nightly) 기능을 사용할 수 있기 때문에 특별한 방식으로 빌드해야 한다는 점이 다릅니다. 표준 라이브러리는 libstd or the "standard facade"라고 불리기도 합니다.
기타
rust-lang/rust 저장소에는 완전한 Rust 배포판을 빌드하는 일과 관련된 다른 것들이 많이 있습니다. 대부분의 경우에는 이것들을 신경 쓸 필요가 없습니다.
여기에는 다음이 포함됩니다.