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

MIR 쿼리와 패스

MIR을 얻으려면 다음과 같이 하십시오.

  • 함수의 경우 optimized_mir 쿼리(주로 코드 생성에서 사용됨)나 mir_for_ctfe 쿼리(주로 컴파일 타임 함수 평가, 즉 CTFE에서 사용됨)를 사용할 수 있습니다.
  • 승격된 값(promoted)의 경우 promoted_mir 쿼리를 사용할 수 있습니다.

이 쿼리들은 최적화가 끝난 최종 MIR을 돌려줍니다. 외부(foreign) def-id의 경우에는 다른 크레이트의 메타데이터에서 MIR을 읽어 오기만 합니다. 하지만 로컬 def-id의 경우, 쿼리는 업스트림 쿼리의 파이프라인을 요청하여 최적화된 MIR을 구성합니다1. 각 쿼리에는 일련의 패스가 들어 있습니다. 이 절에서는 이러한 쿼리와 패스가 어떻게 동작하는지, 그리고 이를 어떻게 확장할 수 있는지 설명합니다.

주어진 def-id D에 대해 최적화된 MIR을 만들기 위해 optimized_mir(D)는 여러 패스 묶음(suite)을 거치며, 각 묶음은 하나의 쿼리로 그룹화되어 있습니다. 각 묶음은 린트, 분석, 변환 또는 최적화를 수행하는 패스들로 이루어져 있습니다. 각 쿼리는 타입 검사나 그 밖의 목적을 위해 MIR 방언(dialect)에 접근할 수 있는 유용한 중간 지점을 나타냅니다.

  • mir_built(D) – 빌드된 직후의 초기 MIR을 제공합니다.
  • mir_const(D) – 몇 가지 간단한 변환 패스를 적용하여 MIR을 const 한정(const qualification)에 대비한 상태로 만듭니다.
  • mir_promoted(D) - 승격 가능한 임시값을 별도의 MIR 본문으로 추출하고, MIR을 빌림 검사에 대비한 상태로 만듭니다.
  • mir_drops_elaborated_and_const_checked(D) - 빌림 검사를 수행하고, (드롭 정교화와 같은) 주요 변환 패스를 실행하며, MIR을 최적화할 준비가 된 상태로 만듭니다.
  • optimized_mir(D) – 활성화된 모든 최적화를 수행하여 최종 상태에 이릅니다.

패스 구현 및 등록

MirPass는 MIR을 처리하는 코드 조각으로, 보통 그 과정에서 MIR을 어떤 식으로든 변환합니다. 하지만 린트(예: MirLint를 구현하는 CheckPackedRef, CheckConstItemMutation, FunctionItemReferences)나 최적화(예: SimplifyCfg, RemoveUnneededDrops) 같은 다른 일을 할 수도 있습니다. 대부분의 MIR 패스는 rustc_mir_transform 크레이트에 정의되어 있지만, MirPass 트레이트 자체는 rustc_middle 크레이트에 들어 있으며, 기본적으로 (tcx와 함께) &mut Body를 받기만 하는 주요 메서드 run_pass 하나로 구성됩니다. 따라서 MIR은 제자리에서 수정됩니다(이는 효율을 유지하는 데 도움이 됩니다).

MIR 패스의 기본적인 예로 RemoveStorageMarkers가 있는데, 이 패스는 MIR을 순회하며 코드 생성 중에 스토리지 마크가 내보내지지 않을 경우 이를 모두 제거합니다. 소스에서 볼 수 있듯이, MIR 패스는 먼저 더미 타입, 즉 필드가 없는 구조체를 정의하는 것으로 정의됩니다.

#![allow(unused)]
fn main() {
pub struct RemoveStorageMarkers;
}

그리고 이 구조체에 대해 MirPass 트레이트를 구현합니다. 그런 다음 이 패스를 mir_built, optimized_mir 등의 쿼리에 있는 적절한 패스 목록에 삽입할 수 있습니다. (최적화 패스라면 optimized_mir 목록에 넣어야 합니다.)

간단한 MIR 패스의 또 다른 예로 CleanupPostBorrowck가 있는데, 이 패스는 MIR을 순회하며 코드 생성과 관련 없는 문장(statement)을 모두 제거합니다. 소스에서 볼 수 있듯이, 이 패스는 먼저 더미 타입, 즉 필드가 없는 구조체를 정의하는 것으로 정의됩니다.

#![allow(unused)]
fn main() {
pub struct CleanupPostBorrowck;
}

그리고 이 구조체에 대해 MirPass 트레이트를 구현합니다.

#![allow(unused)]
fn main() {
impl<'tcx> MirPass<'tcx> for CleanupPostBorrowck {
    fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
        ...
    }
}
}

이 패스는 mir_drops_elaborated_and_const_checked 쿼리 안에서 등록합니다. (최적화 패스라면 optimized_mir 목록에 넣어야 합니다.)

패스를 작성하고 있다면 MIR 방문자를 사용하고 싶어질 가능성이 높습니다. MIR 방문자는 무언가를 검색하거나 작은 수정을 가하기 위해 MIR의 모든 부분을 순회하는 편리한 방법입니다.

훔치기

중간 쿼리인 mir_const()와 mir_promoted()는 tcx.alloc_steal_mir()를 사용해 할당된 &'tcx Steal<Body<'tcx>>를 반환합니다. 이는 결과가 후속 쿼리에 의해 훔쳐질 수 있음을 나타내며, MIR 복제를 피하기 위한 최적화입니다. 훔쳐진 결과를 사용하려고 하면 컴파일러에서 패닉이 발생합니다. 따라서 MIR 처리 파이프라인의 의존성을 고려하지 않은 채 이러한 중간 쿼리에서 실수로 값을 읽지 않도록 하는 것이 중요합니다.

이러한 훔치기 메커니즘 때문에, 처리 파이프라인의 특정 단계에 있는 MIR이 훔쳐지기 전에 그것을 읽으려는 모든 쪽이 이미 읽기를 마쳤는지 주의 깊게 확인해야 합니다.

구체적으로 말하면, mir_promoted(D)의 결과에 접근하려는 쿼리 foo(D)가 있다면 foo(D)가 mir_const(D) 쿼리를 먼저 호출하도록 해야 합니다. 이렇게 하면 그 결과가 직접 필요하지 않더라도 해당 쿼리가 강제로 실행됩니다.

이 메커니즘은 다소 미덥지 못합니다. 더 우아한 대안에 대한 논의는 rust-lang/rust#41710에서 볼 수 있습니다.

개요

다음은 MIR 처리 파이프라인에서 훔치기 의존성의 개요입니다2:

flowchart BT
  mir_for_ctfe* --borrow--> id40
  id5 --steal--> id40

  mir_borrowck* --borrow--> id3
  id41 --steal part 1--> id3
  id40 --steal part 0--> id3

  mir_const_qualif* -- borrow --> id2
  id3 -- steal --> id2

  id2 -- steal --> id1

  id1([mir_built])
  id2([mir_const])
  id3([mir_promoted])
  id40([mir_drops_elaborated_and_const_checked])
  id41([promoted_mir])
  id5([optimized_mir])

  style id1 fill:#bbf
  style id2 fill:#bbf
  style id3 fill:#bbf
  style id40 fill:#bbf
  style id41 fill:#bbf
  style id5 fill:#bbf

진한 색의 스타디움 모양 쿼리(예: mir_built)는 파이프라인의 주요 쿼리이며, 옅은 색의 직사각형 모양 쿼리(예: mir_const_qualif*3)는 &'tcx Steal<Body<'tcx>>에서 결과를 읽어야 하는 후속 쿼리입니다. 훔치기 메커니즘으로 인해, 직사각형 모양 쿼리는 의존성 트리에서 높이가 같거나 더 높은 스타디움 모양 쿼리가 실행되기 전에 먼저 수행되어야 합니다.

예시

예시로 MIR 상수 한정(const qualification)을 생각해 봅시다. 이는 mir_const 쿼리가 생성한 결과를 읽으려 합니다. 하지만 그 결과는 파이프라인의 어느 시점에 mir_promoted 쿼리에 의해 훔쳐집니다. mir_promoted가 쿼리되기 전에는 mir_const_qualif 쿼리 호출이 성공합니다. mir_const가 Steal 결과를 생성하거나(처음 쿼리된 경우) 캐시하고(여러 번 쿼리된 경우), 그 결과가 아직 훔쳐지지 않았기 때문입니다. mir_promoted가 쿼리된 후에는 결과가 훔쳐지므로, 결과를 읽기 위해 mir_const_qualif 쿼리를 호출하면 패닉이 발생합니다.

따라서 이 훔치기 메커니즘에서 mir_promoted는 실제로 훔치기 전에 모든 mir_const_qualif* 쿼리가 호출되도록 보장하여 읽기가 이미 이루어졌음을 확실히 해야 합니다(쿼리는 메모이제이션된다는 점을 기억하십시오. 따라서 쿼리를 두 번 실행하면 두 번째에는 단순히 캐시에서 불러옵니다).


  1. 쿼리의 일반적인 개념은 쿼리 장을 참고하십시오. ↩

  2. mir_promoted 쿼리는 튜플 (&'tcx Steal<Body<'tcx>>, &'tcx Steal<IndexVec<Promoted, Body<'tcx>>>)를 반환하며, promoted_mir는 1번 부분(&'tcx Steal<IndexVec<Promoted, Body<'tcx>>>)을, mir_drops_elaborated_and_const_checked는 0번 부분(&'tcx Steal<Body<'tcx>>)을 훔칩니다. 그리고 이 둘의 훔치기는 서로 무관하므로 따로 수행될 수 있습니다. ↩

  3. 쿼리 이름의 * 접미사는 같은 접두사를 가진 쿼리들의 집합을 나타낸다는 점에 유의하십시오. 예를 들어 mir_borrowck*는 mir_borrowck, mir_borrowck_const_arg, mir_borrowck_opt_const_arg를 나타냅니다. ↩