# Ferro IR `fec` 의 중간 표현. 이 문서는 IR 자체만 다룬다. 언어 규범은 `SPEC.md` 이고, Ferro 의미론이 어떻게 이 형태로 펴지는지는 lowering 이 담당한다. IR 을 두는 이유는 하나다. **검사가 끝난 AST 와 기계 사이의 거리가 너무 멀다.** `try` 하나가 분기 두 개와 임시값 하나로 펴지고, `defer` 는 함수의 모든 이탈 경로에 복제되며, 배열 인덱스는 비교와 트랩을 낳는다. 그 전개를 명령어 선택과 같은 자리에서 하면 둘 다 읽을 수 없게 된다. --- ## 1. 형태 함수 단위다. 함수는 **기본 블록**의 목록이고, 블록은 **명령**의 목록과 하나의 **종결자**로 끝난다. 블록 중간에서 분기하지 않고, 종결자 뒤에 명령이 없다. ``` fn @okid.test() -> i32 { $0: i32 ; 지역 b0: %0 = const i32 7 store $0, %0 %1 = load i32 $0 ret %1 } ``` ### 슬롯 | | | | |---|---|---| | `%n` | 임시값 | 한 번 정의되고 여러 번 쓰인다. 블록을 넘지 않는다 | | `$n` | 지역 | 주소를 가진 스택 자리. 함수 진입 시 전부 잡는다 | | `@name` | 전역·함수 | 링커가 보는 이름 | 임시값이 블록을 넘지 않으므로 φ 노드가 없다. 블록을 넘겨야 하는 값은 지역에 `store` 하고 다시 `load` 한다. 이것이 SSA 보다 코드를 조금 더 만들지만, **레지스터 할당기를 블록 단위로 유지**해 준다. v0.1 에서는 그 교환이 맞다. ### 기계 타입 ``` i8 i16 i32 ptr mem ``` Ferro 타입은 여기서 사라진다. 구조체·배열·슬라이스·옵셔널·에러 유니온은 전부 `mem` 이고, 필드 접근은 lowering 이 계산한 **바이트 오프셋**이다. IR 은 `Box(i32)` 라는 것을 모른다 — 크기 N 바이트짜리 메모리만 안다. `ptr` 은 4바이트다. 세그먼트가 없으므로 포인터 종류도 하나뿐이다 (`SPEC.md` §2). `bool` 은 `i8`, `char` 는 `i8`, `usize` 는 `i32` 다. --- ## 2. 명령 12개다. ``` %d = const 상수 %d = load 메모리에서 읽는다 store , %v 메모리에 쓴다 %d = addr 주소를 뜬다 %d = %a, %b 산술·비트 연산 %d = %a, %b 비교. 결과는 i8 %d = cast %a 정수 폭 변환 %d = call @f(%a, ...) 호출 copy , , N N바이트 복사 ``` `` — `add sub mul div mod and or xor shl shr` `` — `eq ne lt le gt ge` (부호 있음/없음은 `` 가 정한다) ### place `load`·`store`·`addr`·`copy` 의 피연산자다. ``` $n 지역 @name 전역 %p 포인터 임시값이 가리키는 곳 %p + 상수 오프셋. 필드 접근이 여기로 온다 ``` 인덱스처럼 오프셋이 상수가 아니면 lowering 이 주소를 먼저 계산한다. ``` ; a[i] 는 %0 = load i32 $i %1 = const i32 4 ; 원소 크기 %2 = mul i32 %0, %1 %3 = addr $a %4 = add ptr %3, %2 %5 = load i32 %4 ``` ### 덩어리 **덩어리는 언제나 주소로 오간다.** 크기 임계값이 없다. ``` call @f(%p) ; f 가 mem 을 받으면 %p 는 그 주소다 ``` 반환도 같다. `mem` 을 반환하는 함수는 **첫 인자로 결과를 쓸 주소를 받는다**. 호출자가 자리를 잡고 넘긴다. 이 규약을 고른 이유는 단순해서만이 아니다. ISA 마다 다른 "구조체를 언제 레지스터로 넘기는가" 규칙을 통째로 피해간다. `extern "c"` 경계에서는 C ABI 로 변환해야 하며 그것은 백엔드의 일이다. --- ## 3. 종결자 4개다. ``` jmp b 무조건 분기 br %c, b, b %c 가 0이 아니면 b ret [%v] 반환 trap 중단 ``` `trap` 은 `fe_trap(reason, UNIT_FILE, line)` 이 된다. `reason` 은 작은 정수이고 `UNIT_FILE` 은 **유닛당 하나뿐인 파일 이름 문자열**이다. 트랩 지점마다 문자열을 두면 실행 파일이 부풀기 때문이다. `line` 은 컴파일 시점에 상수로 박힌다. 주소가 아니라 줄 번호를 남기는 방식은 당대에 흔했고, 심볼 테이블 없이 실패 지점을 말할 수 있는 가장 싼 방법이다. reason 값: | | | |---|---| | 0 | 배열·슬라이스 경계 | | 1 | 정수 오버플로 | | 2 | 0으로 나눔 | | 3 | 도달할 수 없는 곳에 도달 (`@unreachable`) | | 4 | 명시적 `@trap()` | `--no-checks` 는 0·1·2 를 만드는 검사를 lowering 단계에서 생략한다. 3·4 는 소스에 쓰인 것이므로 남는다. --- ## 4. 함수와 전역 ``` fn @unit.name(, ...) -> { ... } extern fn @name(, ...) -> global @unit.name : mem = <초기값 바이트> ``` 이름은 `유닛.이름` 이다. 제네릭 인스턴스도 여기서는 그냥 함수 하나다 — 모노모피제이션이 프론트엔드에서 끝나므로 **IR 에 제네릭이라는 개념이 없다.** --- ## 5. 이 IR 이 하지 않는 것 - **최적화 없음.** 상수 접기도, 죽은 코드 제거도 없다. 나중에 붙일 자리는 있다 - **φ 노드 없음.** 블록 간 값은 지역을 경유한다 - **타입 검사 없음.** 검사는 프론트엔드에서 끝났다. IR 이 잘못되었다면 lowering 의 버그다 - **예외·언와인딩 없음.** 에러는 값이고, `try` 는 분기다 --- ## 6. 왜 이 크기인가 명령 12개와 종결자 4개는 **i386 으로 직접 번역할 수 있는 최소 집합**이다. 각각이 몇 개의 x86 명령으로 내려가고, 그 대응이 눈으로 확인된다. 이보다 높으면 백엔드가 IR 을 다시 해석해야 하고, 이보다 낮으면 lowering 이 기계에 가까워져서 다른 ISA 로 옮길 때 다시 써야 한다. 같은 이유로 이 집합은 m68k·ARM·MIPS·RV32 에도 그대로 내려간다. 평평한 주소 공간과 정수 연산만 쓰기 때문이다. 백엔드를 하나 더 만드는 비용은 명령 선택과 레지스터 할당이지 IR 재설계가 아니다.