8.8. Lowering: 데이터 타입과 표현식
이 절에서는 rok 의 값과 타입, 그리고 식이 MLIR 표준 다이얼렉트로 어떻게 lower 되는지를 정의합니다.
8.8.1. 원시 정수 / 부동소수
원시 수치 타입은 다음과 같이 MLIR 타입으로 매핑됩니다.
| rok 타입 | MLIR 타입 |
|---|---|
i8 ~ i64 | i8 ~ i64 |
u8 ~ u64 | i8 ~ i64 (sign 은 op 가 결정) |
usize / isize | index 또는 타깃별 iN |
f32 / f64 | f32 / f64 |
bool | i1 (또는 i8, ABI 에 따라) |
char | i32 (Unicode 코드포인트) |
c_char | i8 |
str 은 !llvm.struct<(ptr, iN)> (포인터와 usize 바이트 길이) 로 값 전달되는 불변 쌍입니다. 리터럴은 immortal 상수를, 런타임에 만든 문자열은 GC 객체 (type id 1, 8.2.12) 의 payload 를 가리킵니다.
s[a:b]는(ptr + a, b - a)이며 원래 바이트를 공유합니다 (O(1)).ptr은 GC 객체의 어느 바이트든 (끝의 NUL 포함) 가리킬 수 있고, 객체 전체를 살려 둡니다. GC 는 이를 내부 포인터로 다루며, evacuation 은 offset 을 지켜 고쳐 씁니다.- 리터럴과 런타임이 새로 만든 문자열은 바이트 뒤에 NUL 이 있습니다. slice 는 없을 수 있습니다.
여기서 한 가지 유의할 점이 있습니다. MLIR 의 정수 타입 자체에는 부호 정보가 없으며, 부호 해석은 연산자 (addi 나 subi 등) 가 결정합니다.
8.8.2. 산술 연산
산술 식은 아래와 같이 arith 다이얼렉트의 연산으로 lower 됩니다.
a + b → arith.addi %a, %b : i32
a - b → arith.subi %a, %b : i32
a * b → arith.muli %a, %b : i32
a / b → arith.divsi %a, %b : i32 (signed) / divui (unsigned)
a % b → arith.remsi / remui
a + b (f64) → arith.addf %a, %b : f64
a < b → arith.cmpi "slt", %a, %b : i32 (signed less than)
또는 arith.cmpf "olt", %a, %b : f64
연산자 디스패치 의 트레잇 매핑은 원시 타입에 대해 위와 같이 lower 됩니다. 한편 사용자 정의 트레잇 구현은 제네릭 단형화를 거친 뒤 함수 호출로 lower 됩니다.
8.8.3. struct 의 표현
struct 는 값 타입이며 (2.3.8) 객체 헤더가 없습니다.
struct Point { x: f64; y: f64; }
필드들로 된 LLVM struct 로 lower 되며, 배치는 2.3.6 을 따릅니다.
!Point = !llvm.struct<"Point", (f64, f64)>
구조체 값은 SSA 레지스터, 스택 슬롯, 또는 담는 쪽 안에 inline 으로 있습니다: 다른 구조체의 필드, List 버퍼의 원소, 클로저 환경이나 coroutine frame 의 슬롯. 대입과 인자 전달은 필드를 복사합니다. 컴파일러는 관찰할 수 없는 곳에서만 값을 내부적으로 box 할 수 있습니다.
필드 접근
SSA 값의 p.x 는 extractvalue 이고, 스택 슬롯이나 inline 위치에서는 필드 주소를 통한 load 입니다.
%val = llvm.extractvalue %p[0] : !Point
%addr = llvm.getelementptr %slot[0, 0] : (!llvm.ptr) -> !llvm.ptr, !Point
%val2 = llvm.load %addr : !llvm.ptr -> f64
필드 쓰기
p.x = 1.0f64 는 변수 자신의 사본을 바꿉니다: SSA 값이면 insertvalue, 슬롯이면 필드 주소로의 store 입니다.
%p2 = llvm.insertvalue %one, %p[0] : !Point
구조체가 GC heap 객체 안에 inline 으로 있고 필드가 GC 참조를 가지면, 그 store 는 write barrier 를 거칩니다 (배리어와 루트). 스택 슬롯에 있는 구조체의 포인터 필드는 GC 루트입니다 (7.4).
8.8.4. tuple struct 의 표현
다음과 같은 tuple struct 를 살펴보겠습니다.
struct Pair(i32, i32);
이는 일반 struct 와 동일하게 표현되며, 다만 필드 이름 대신 위치 인덱스로 접근합니다.
8.8.5. tuple 타입
다음과 같은 tuple 타입은
(i32, str, bool)
!llvm.struct<(i32, !llvm.ptr, i1)> 로 lower 됩니다. struct 처럼 헤더 없는 값입니다.
8.8.6. enum 의 표현
enum 의 enum 은 tagged union 형태로 lower 됩니다.
enum Result {
Ok(i32),
Err(str),
Pending,
}
이는 다음과 같이 표현됩니다.
!Result = !llvm.struct<"Result", (
i32, // type_id
i32, // gc_flags
i64, // gc_word
i32, // discriminant (tag)
!llvm.struct<(i64)> // 가장 큰 변형의 payload (8 바이트)
)>
여기서 discriminant 는 변형 식별자 (Ok=0, Err=1, Pending=2) 를 담고, payload 는 모든 변형의 union 으로서 가장 큰 변형의 크기를 갖습니다.
변형 생성
변형을 생성하는 식은 다음과 같습니다.
Result::Ok(42)
이는 다음 MLIR 시퀀스로 lower 됩니다.
; struct 할당 + 헤더 채우기 + tag 채우기 + payload 채우기
%obj = call @rok_gc_alloc_slow(...)
; ... 헤더 ...
%tag_addr = llvm.getelementptr %obj [0, 3] : ...
llvm.store i32 0, %tag_addr ; Ok = 0
%payload_addr = llvm.getelementptr %obj [0, 4] : ...
llvm.store %42_i32, %payload_addr
더 짧은 변형의 payload 로 만든 union 값은 그 payload 뒤의 바이트에 0 을 담습니다. 그래서 거기서 복사한 root word 는 null 이나 참조이며, 결코 오래된 스택 값이 아닙니다 (8.9.12).
변형 매칭
변형을 매칭하는 식을 살펴봅니다.
match v {
Ok(n) => ...,
Err(s) => ...,
Pending => ...,
}
이는 decision tree 형태의 MLIR 로 lower 됩니다.
%tag = llvm.load %v[3] : i32
cf.switch %tag, [
default: ^unreachable, ; 모든 변형이 case 에 있어 default 도달 안 함
0: ^ok,
1: ^err,
2: ^pending
]
^ok:
%payload = llvm.load %v[4] : i32
; ... n 으로 사용 ...
br ^end
^err:
%payload_str = llvm.load %v[4] : !llvm.ptr
; ... s 로 사용 ...
br ^end
^pending:
; ...
br ^end
8.8.7. union (비태그) 의 표현
union 의 union 을 살펴보겠습니다.
union EpollData {
ptr: usize;
fd: i32;
val: u64;
}
이는 가장 큰 필드 크기를 갖는 !llvm.struct<(i64)> 로 lower 됩니다. 각 필드 접근은 bitcast 나 getelementptr 를 통해 같은 메모리를 다른 형으로 해석하는 방식으로 처리됩니다. 필드 읽기는 소스에서 unsafe 안에 있어야 합니다 (2.5).
8.8.8. 합 타입 (A | B) 의 표현
합 타입 의 합 타입은 다음과 같이 다룹니다.
type X = i32 | f64
이는 !llvm.struct<(i32, i64)> 로 표현되며, tag (i32) 와 payload (i64, 가장 큰 변형 크기) 의 조합입니다.
만약 niche 최적화 (niche 최적화) 가 적용된다면 tag 자리가 사라지고 합집합 비트 패턴만 남게 됩니다.
Ptr<u8> | none
위 타입은 !llvm.ptr (단일 포인터, null 이 곧 none 을 의미하는 자동 niche) 로 lower 됩니다.
8.8.9. any 슬롯의 표현
any 슬롯 의 동적 슬롯을 살펴봅니다.
let x: any = ...
이는 (i32 tag, usize payload) 쌍으로 lower 되며, tag 는 type_id 입니다. any Tr 과 맨 any 의 표현은 같습니다.
!Any = !llvm.struct<(i32, iN)> ; iN = 타깃의 포인터 폭 (usize)
포인터 폭 이하의 숫자·bool·char 값은 payload 에 직접 담깁니다. 그 밖의 값은 GC heap 의 box 이고 payload 는 그 포인터입니다. 따라서 payload 가 포인터인지는 tag 로만 알 수 있으며, GC 는 any 슬롯을 scan 할 때 tag 를 보고 판단해야 합니다.
any 의 동적 디스패치
다음 식을 봅니다.
v + 1i32 // v: any
이는 런타임 검사를 동반하는 다음 MLIR 로 lower 됩니다.
%tag = llvm.extractvalue %v[0] : i32
cf.switch %tag, [
default: ^trap,
TAG_I32: ^add_i32,
TAG_F64: ^add_f64, ; 다른 피연산자 타입 불일치는 ^trap (panic, exit 101)
...
]
^add_i32:
%payload = llvm.extractvalue %v[1] : i64 ; 64-bit 타깃
%i = llvm.trunc %payload : i64 to i32
%sum = arith.addi %i, %1_const : i32
; 결과를 any 로 다시 wrap
...
이러한 분기 구조는 런타임에 모든 가능한 타입을 검사해야 한다는 점에서, any 슬롯이 가지는 비용이라 할 수 있습니다.
8.8.10. 배열 타입: [T; N]
타입 표현식 의 고정 크기 배열을 살펴보겠습니다.
[i32; 8]
이는 !llvm.array<8 x i32> 로 lower 됩니다. GC 객체 안에 자리하는 경우라면 16 바이트 헤더가 배열 앞에 붙습니다.
8.8.11. 함수 타입: fn(...) -> ...
함수 타입 을 살펴봅니다.
fn(i32, i32) -> i32
이는 (i32, i32) -> i32 타입의 함수 포인터, 즉 !llvm.ptr 또는 !llvm.func<...> 로 lower 됩니다. 함수 포인터 값은 값으로서 변수에 저장될 수 있습니다.
8.8.12. 클로저의 표현
클로저 중 캡처가 있는 클로저를 살펴봅니다.
let n = 10;
let f = |x| x + n; // n 캡처
이는 다음과 같은 MLIR 로 lower 됩니다.
; 클로저 객체 = 환경 + 코드 포인터
!Closure = !llvm.struct<(
i32, i32, i64, ; GC header
!llvm.ptr, ; 함수 포인터 (코드)
i32 ; 환경: n 의 복사본
)>
; 호출 시
%env = llvm.getelementptr %clo[0, 4] : ...
%fn_ptr = llvm.extractvalue %clo[0, 3] : !llvm.ptr
%result = llvm.call %fn_ptr(%env, %x) : ...
호출 시 환경은 함수의 추가 첫 인자로 전달됩니다.
8.8.13. 식별자 / 변수 접근
변수 선언 의 지역 변수를 살펴보겠습니다.
let x = 42i32;
let y = x + 1i32;
이는 LLVM 의 SSA 형태로 lower 됩니다.
%x = arith.constant 42 : i32
%y = arith.addi %x, %1 : i32
지역 변수는 SSA 로 lower 하며, 스택 슬롯이 필요한 지역은 llvm.alloca 와 load / store 로 둡니다 (debug / release 같음).
let counter = 0i32;
counter = counter + 1i32;
이는 다음과 같이 lower 됩니다.
%counter_ref = llvm.alloca %c1 x i32 : (i64) -> !llvm.ptr
llvm.store %0, %counter_ref : i32, !llvm.ptr
%loaded = llvm.load %counter_ref : !llvm.ptr -> i32
%new = arith.addi %loaded, %1 : i32
llvm.store %new, %counter_ref : i32, !llvm.ptr
최적화 빌드에서는 LLVM 의 mem2reg 가 이를 다시 SSA 로 올립니다.
8.8.14. 리터럴 desugar
#[literal] 매직 의 컬렉션 리터럴을 살펴봅니다.
[1, 2, 3]
이는 표준 라이브러리의 list_literal(1, 2, 3) 함수 호출로 desugar 되어, 일반적인 함수 호출 lowering 을 따라갑니다.
한편 문자열과 숫자 리터럴은 컴파일 타임 const 로 처리되어 .rodata 에 저장되며, 이후에는 포인터 또는 값으로 사용됩니다.