MIR 구성
HIR에서 MIR로의 로워링은 다음 항목들에 대해 일어납니다(이 목록은 완전하지 않을 수 있습니다):
- 함수 및 클로저 본문
static및const항목의 초기화 식- 열거형 판별값의 초기화 식
- 모든 종류의 글루(glue)와 심(shim)
- 튜플 구조체 초기화 함수
- 드롭 코드(
Drop::drop함수는 직접 호출되지 않습니다) - 명시적인
Drop구현이 없는 타입의 드롭 구현
로워링은 mir_built 쿼리를 호출하면 시작됩니다. MIR 빌더는 실제로 HIR을 사용하지 않고 THIR에서 동작하며, THIR 표현식을 재귀적으로 처리합니다.
로워링은 먼저 시그니처에 명시된 모든 인자에 대해 지역 변수를 생성합니다. 다음으로, 지정된 모든 바인딩에 대해 지역 변수를 생성합니다. 예를 들어 (a, b): (i32, String)은 바인딩 3개를 만드는데, 하나는 인자에 대한 것이고 두 개는 바인딩에 대한 것입니다. 그다음에는 인자에서 필드를 읽는 필드 접근을 생성하고, 그 값을 바인딩 변수에 씁니다.
이 초기화가 끝나면, 로워링은 본문(Block 표현식)에 대한 MIR을 생성하는 함수를 재귀적으로 호출하고, 그 결과를 RETURN_PLACE에 씁니다.
모든 것에 unpack!을
MIR을 생성하는 함수는 대개 두 가지 패턴 중 하나에 속합니다. 첫째, 함수가 문(statement)만 생성한다면 그 문들을 덧붙일 기본 블록을 인자로 받습니다. 그런 다음 평소처럼 결과를 반환할 수 있습니다:
fn generate_some_mir(&mut self, block: BasicBlock) -> ResultType {
...
}
하지만 새로운 기본 블록까지 생성할 수 있는 함수들도 있습니다. 예를 들어 if foo { 22 } else { 44 } 같은 표현식을 로워링하려면 작은 “다이아몬드 모양 그래프“를 생성해야 합니다. 이 경우 함수는 코드가 시작되는 기본 블록을 받고, 코드 생성이 끝나는 (새로운 것일 수도 있는) 기본 블록을 반환합니다. 이를 표현하는 데 BlockAnd 타입이 사용됩니다:
fn generate_more_mir(&mut self, block: BasicBlock) -> BlockAnd<ResultType> {
...
}
이러한 함수를 호출할 때는 사실상 “커서” 역할을 하는 지역 변수 block을 두는 것이 일반적입니다. 이 변수는 새 MIR을 추가하고 있는 지점을 나타냅니다. generate_more_mir를 호출할 때는 이 커서를 갱신해야 합니다. 이를 수동으로 할 수도 있지만 번거롭습니다:
let mut block;
let v = match self.generate_more_mir(..) {
BlockAnd { block: new_block, value: v } => {
block = new_block;
v
}
};
이러한 이유로 let v = unpack!(block = self.generate_more_mir(...))처럼 작성할 수 있게 해 주는 매크로를 제공합니다. 이 매크로는 단순히 새 블록을 추출하여 unpack!에서 지정한 변수 block을 덮어씁니다.
표현식을 원하는 MIR로 로워링하기
표현식에 대해 원할 수 있는 표현 형태는 본질적으로 네 가지가 있습니다:
Place는 이미 존재하는 메모리 위치(또는 그 일부)를 가리킵니다(지역 변수, 정적 변수, 승격된 값).Rvalue는Place에 대입할 수 있는 것입니다Operand는+연산이나 함수 호출 등의 인자입니다- 값의 복사본을 담고 있는 임시 변수입니다
다음 이미지는 각 표현 사이의 상호작용을 개략적으로 보여 줍니다:
RETURN_PLACE에 대한 대입을 만들 수 있도록, 먼저 함수 본문을 Rvalue로 로워링합니다. 이 Rvalue 로워링은 다시 인자(있는 경우)를 Operand로 로워링하도록 합니다. Operand 로워링은 const 피연산자를 만들거나, Place에서 값을 이동 또는 복사하며, 후자의 경우 Place 로워링이 일어납니다. Place로 낮춰지는 표현식은 연산을 포함하고 있으면 다시 임시 값의 생성을 일으킬 수 있습니다. 바로 여기서 뱀이 자기 꼬리를 무는 상황이 됩니다. 지역 변수에 쓸 표현식에 대해 Rvalue 로워링을 일으켜야 하기 때문입니다.
연산자 로워링
내장 타입에 대한 연산자는 함수 호출로 낮춰지지 않습니다(트레이트 impl에는 그 연산 자체가 다시 들어 있을 뿐이므로, 그렇게 하면 결국 무한 재귀 호출이 됩니다). 대신 바이너리 연산자와 단항 연산자, 그리고 인덱스 연산을 위한 Rvalue가 있습니다. 이 Rvalue들은 이후 llvm 기본 연산이나 llvm 내장 함수(intrinsic)로 코드 생성됩니다.
그 밖의 모든 타입에 대한 연산자는 해당 연산자에 대응하는 트레이트의 impl에 대한 함수 호출로 낮춰집니다.
로워링 종류와 관계없이 연산자의 인자는 Operand로 로워링됩니다. 즉, 모든 인자는 상수이거나, 로컬 또는 정적 변수 어딘가에 이미 존재하는 값을 가리킵니다.
메서드 호출 로워링
메서드 호출은 함수 호출과 동일한 TerminatorKind로 낮춰집니다. MIR에서는 더 이상 메서드 호출과 함수 호출 사이에 차이가 없습니다.
조건
if 조건과, 필드가 없는 배리언트만 가진 enum에 대한 match 문은 TerminatorKind::SwitchInt로 낮춰집니다. 가능한 각 값(if 조건의 경우 0과 1)마다 코드가 이어서 실행될 BasicBlock이 하나씩 대응합니다. 분기의 기준이 되는 인자는 (역시) if 조건의 값을 나타내는 Operand입니다.
패턴 매칭
필드가 있는 배리언트를 가진 enum에 대한 match 문 역시 TerminatorKind::SwitchInt로 낮춰지지만, 이때 Operand는 값의 판별값(discriminant)을 찾을 수 있는 Place를 가리킵니다. 이를 위해 판별값을 새 임시 변수로 읽어 들이는 경우가 많습니다.
집합체 생성
모든 종류의 집합체 값(예: 구조체나 튜플)은 Rvalue::Aggregate를 통해 만들어집니다. 모든 필드는 Operator로 낮춰집니다. 이는 본질적으로 집합체 필드마다 대입문을 하나씩 두고, enum의 경우 판별값에 대한 대입을 하나 더 두는 것과 같습니다.