부록 D: 런타임 ABI
이 부록에서는 최종 네이티브 컴파일러와 fun-rt-24를 설명합니다. 지원 호스트는 64비트 워드와 포인터를 사용하는 Apple Silicon macOS와 x86-64/AArch64 Linux입니다. 이 ABI는 이번 판의 컴파일러 내부에서만 쓰입니다. 사용자가 작성하는 FFI는 없습니다.
워드와 객체
호출 경계를 넘는 모든 소스 값은 LLVM에서는 i64, Rust에서는 u64입니다. 최하위 비트가 1이면 즉시값(immediate)을 나타냅니다. 최하위 비트가 0이면 정렬된 객체 주소를 나타냅니다. 단, 초기화 중에 임시로 쓰이는 0 자리 표시자는 예외입니다.
| 값 | 인코딩 |
|---|---|
Int | (n << 1) | 1, 부호 있는 63비트 2의 보수 |
Char | 왼쪽으로 1비트 시프트하고 최하위 비트를 1로 설정한 유니코드 스칼라 |
| 인자 없는 생성자 | (tag << 1) | 1 |
| 힙 값 | 8바이트로 정렬된 헤더의 주소 |
해석은 기대 타입에 따라 정해집니다. 따라서 False, [], Nothing, ()는 모두 워드 값 1을 가질 수 있지만 서로 바꿔 쓸 수는 없습니다.
header: [ payload size: 32 bits | constructor tag: 24 bits | kind: 8 bits ]
object: [ header ][ payload word 0 ][ payload word 1 ] ...
크기는 페이로드 워드의 개수입니다. 생성자 태그는 각 데이터 타입 안에서만 유효하며 선언 순서를 따릅니다. 고정된 Prelude 태그는 False=0, True=1, []=0, (:)=1, Nothing=0, Just=1, ()=0, 그리고 LT=0, EQ=1, GT=2입니다.
| 종류 | ID | 페이로드 | 추적 필드 |
|---|---|---|---|
CON | 0 | 생성자 필드 | 전부 |
CLOSURE | 1 | 느린 코드 주소, 원시 항수, 캡처 | 캡처 |
PAP | 2 | 클로저 워드, 원시 인자 개수, 인자들 | 클로저와 인자들 |
DOUBLE | 3 | Binary64 비트 | 없음 |
STRING | 4 | 원시 바이트 길이, 워드 단위로 패딩된 UTF-8 바이트 | 없음 |
FORWARD | 5 | 수집 중의 목적지 주소 | 수집기 전용 표식 |
인자 없는 생성자에는 힙 할당이 없습니다. 따라서 이동 가능한 모든 객체에는 포워딩 주소를 담을 공간이 있습니다. 문자열은 저장 시에는 바이트 수를 세고, length 계열 프리미티브에서는 유니코드 스칼라 수를 셉니다.
진입점과 적용
| 진입점 | 호출 규약과 시그니처 |
|---|---|
| 알려진 함수 | tailcc i64 f(i64 env, i64 a1, ..., i64 an) |
| 느린 어댑터 | C: u64 slow(u64 closure, const u64 *args) |
| 일반 적용 | C: u64 rt_apply(u64 closure, usize n, const u64 *args) |
| 소규모 적용 | C: u64 rt_applyN(u64 closure, u64 a1, ..., u64 aN), N=1..4 |
| PAP 생성 | C: u64 rt_make_pap(u64 closure, usize n, const u64 *args) |
| 재귀 캡처 패치 | C: void rt_closure_set(u64 closure, usize index, u64 value) |
느린 어댑터는 정확히 함수의 구문적 항수만큼 인자를 받습니다. 일반 적용 루틴은 PAP 인자를 평탄화하거나, 정확한 호출을 수행하거나, 남은 인자를 반환된 함수에 적용합니다. 네이티브 꼬리 전달이 보장되는 것은 알려진 꼬리 호출뿐입니다. C와 tailcc 사이를 넘나드는 호출은 ABI 어댑터 프레임을 유지합니다.
모든 인자는 엄격하며 이미 평가된 상태입니다. 원시 인자 배열은 호출이 진행되는 동안 읽을 수 있는 상태로 남아 있어야 합니다. 수집기는 배열의 값을 재배치할 수 있으므로, 지속되는 버퍼는 루트 소유자에게 속합니다. 느린 진입점은 어떤 할당보다도 먼저 인자를 불러오고 생성된 루트 프레임을 설정합니다.
할당과 루트
| 내보내는 함수 | 계약 |
|---|---|
void rt_init() | Boehm을 초기화하고 호출한 런타임 스레드를 등록 |
u64 *rt_alloc(usize words) | 헤더를 포함하여 스캔 대상 저장 공간을 확보 |
u8 *rt_alloc_atomic(usize bytes) | 포인터가 없는 저장 공간을 확보 |
void rt_use_copying() | 초기화 전에 단일 스레드 이동 힙을 선택 |
void rt_roots_push(u64 *slots, usize n) | 쓰기 가능하고 초기화된 생성 루트 범위를 등록 |
void rt_roots_pop() | 현재 생성 루트 범위를 제거 |
void rt_global_root(u64 *slot) | 프로세스 수명 동안 정적 전역 변수를 등록 |
할당은 반환하기 전에 수집을 수행할 수 있습니다. 호출자는 추가 할당을 하기 전에 새 객체 전체를 초기화합니다. 네이티브 시작 과정은 함수를 설치하거나 값을 평가하기 전에 전역 변수를 등록합니다. 정적 문자열 객체는 불변이며 이동하지 않습니다. Boehm 백엔드는 이들의 주소 테이블도 유지합니다.
Rust의 Roots와 GlobalRoot 소유자는 drop될 때까지 안정적인 버퍼를 등록합니다. 이동 수집기는 이들의 셀을 갱신합니다. 빌린 문자열 슬라이스와 루트로 등록되지 않은 포인터 워드의 복사본은 할당 이후까지 살아남아서는 안 됩니다. 프리미티브 익스포트는 결과를 할당하기 전에 문자열을 소유된 Rust 값으로 디코딩합니다.
프리미티브, 오류, 종료
모든 prim_* 연산에는 rt_prim_*라는 이름의 C 익스포트가 있습니다. 각 인자와 반환 값은 워드이며, unit 결과도 마찬가지입니다. Integer와 Double 산술 연산은 대신 인라인으로 생성될 수도 있습니다. 간단하지 않은 연산은 안전한 Rust 구현을 인터프리터와 공유합니다.
| 프리미티브 계열 | 인자 | 결과 |
|---|---|---|
intAdd/Sub/Mul/Quot/Rem/Div/Mod | Int 워드 두 개 | Int 워드 |
intNeg | Int | Int |
intEq/Lt/Le | Int 워드 두 개 | Bool 즉시값 |
dblAdd/Sub/Mul/Div/Pow | 박싱된 Double 두 개 | 박싱된 Double |
dblNeg/Sqrt/Exp/Log/Sin/Cos | 박싱된 Double | 박싱된 Double |
dblEq/Lt/Le | 박싱된 Double 두 개 | Bool 즉시값 |
intToDbl, dblFloor/Round/Truncate | 변환할 원본 수치 값 | 변환된 수치 값 |
chrOrd, chrChr, chrEq/Lt | 문자/정수 피연산자 | 정수/문자/Bool |
strAppend/Length/Eq/Compare | 문자열 한 개 또는 두 개 | 문자열/Int/Bool/Int |
strUnpack, strPack | 문자열 또는 문자 리스트 | 문자 리스트 또는 문자열 |
showInt/Double/Char/String | 이름에 해당하는 타입의 값 하나 | 문자열 |
readInt | 문자열 | Maybe Int |
putStr, getLine | 문자열 또는 unit | Unit 또는 문자열 |
trace | 문자열 메시지, 임의의 값 | 같은 값 |
error, exit | 문자열 또는 Int | 반환하지 않음 |
정수 결과는 2⁶³을 법으로 하여 순환합니다. 부동소수점 변환은 Int 범위로 포화되며, NaN은 0이 됩니다. 0으로 나누기, 잘못된 문자, getLine에서의 EOF, 매치 실패는 런타임 오류입니다.
| 내보내는 함수 | 동작 |
|---|---|
rt_error(u64 string) | stdout을 플러시하고 fun: MESSAGE를 출력한 뒤 1로 종료합니다 |
rt_pattern_fail(const char *file, u32 line, u32 col) | 소스 위치를 보고하고 1로 종료합니다 |
rt_div_zero() | fun: divide by zero를 보고하고 1로 종료합니다 |
rt_exit(i32 code) | stdout을 플러시하고 종료합니다 |
생성된 C main은 런타임을 초기화하고, 전역 값을 의존성 순서대로 평가하고, 소스의 main을 유닛 워드 1에 적용한 뒤, 성공적으로 종료합니다. 런타임 패닉은 생성된 코드를 거슬러 되감기(unwinding)하지 않고 즉시 중단(abort)합니다.