25장: 향후 과제
우리는 계산기에서 출발했습니다. 이제 우리에게는 타입이 추론되고, 대수적 데이터와 레코드, 타입 클래스를 갖춘 언어가 있고, 네이티브 코드를 생성하는 컴파일러도 있습니다. 최적화기와 가비지 컬렉터까지 만들었습니다. 꽤 긴 여정이었습니다!
가장 좋은 점은 이제 이 컴파일러가 마음껏 실험해 볼 수 있는 여러분의 것이 되었다는 것입니다. 여러분은 컴파일러의 결정이 어디에서 내려지는지 알고 있으며, 다른 접근 방식을 시도해 보고 싶을 때 돌아갈 수 있는 더 작은 장별 버전도 가지고 있습니다. 다음으로 Fun을 가지고 탐구해 보면 재미있을 만한 몇 가지 방향을 소개합니다.
다음은 어디로?
명시적 지연과 지연 평가
때로는 정말로 계산을 기술만 해 두고 나중으로 미루고 싶을 때가 있습니다. 저라면 값과 계산을 구분하는 call-by-push-value를 사용해 명시적 지연을 통해 이를 탐구해 보겠습니다 (Levy 2001). 그러면 프로그래머는 계산이 어디에서 중단되고 어디에서 강제되는지 볼 수 있습니다.
결과를 공유하는 것은 또 다른 선택입니다. 필요에 의한 호출에는 서스펜션이 이미 평가되었다는 사실을 기억하는 방법과 재귀적 강제를 처리하는 방법이 필요합니다. 우리의 클로저 표현은 유용한 출발점이지만, 썽크 객체를 추가하는 것만으로는 그 모든 일을 해낼 수 없습니다. STG와 비슷한 중간 언어가 있다면 이러한 연산이 자리 잡을 곳이 생길 것입니다.
모듈과 분할 컴파일
프로그램이 커지면 모든 것을 한 파일에 넣는 일에 아마 지치게 될 것입니다. 모듈이 있으면 정의를 묶고, 인터페이스를 공개하고, 나머지는 숨길 수 있습니다. 분할 컴파일이 있으면 프로그램 전체를 다시 컴파일하지 않고도 한 부분만 다시 빌드할 수 있습니다.
우리는 이미 한 번의 컴파일 안에서는 고유한 이름을 가지고 있습니다. 다음 단계는 어떤 식별자와 타입 정보가 컴파일 사이에 살아남아야 하는지 정하는 것입니다. 클래스가 있으면 이 문제가 특히 흥미로워집니다. 가져온 함수는 인스턴스에 의존할 수 있으므로, 그 함수가 어떤 인스턴스를 보게 되는지에 대한 명확한 규칙이 필요합니다. 명시적 인터페이스를 갖춘 작은 모듈 시스템이 좋은 첫 실험이 될 것입니다.
행, 효과, 클래스
우리의 레코드에는 이름과 고정된 필드가 있습니다. 함수가 x 필드를 가진 레코드라면 무엇이든 받을 수 있다면 어떨까요? 행 다형성(row polymorphism)은 이를 표현하는 한 가지 방법입니다. 효과 행(effect row)은 또 다른 탐구의 길을 열어 주며, 계산이 수행할 수 있는 효과가 무엇인지 타입으로 기술할 수 있게 해 줍니다.
이런 아이디어는 우리가 만든 제약 언어의 범위를 넘어서겠지만, 기본적인 습관은 여전히 통합니다. 표현하고 싶은 작은 프로그램을 찾고, 의도한 타입을 적어 본 다음, 추론에 어떤 정보가 필요한지 알아내십시오.
다중 매개변수 클래스, 함수적 종속성(functional dependency), 타입 패밀리(type family)를 탐구해 볼 수도 있습니다. 우리가 둔 Haskell 98 제약 덕분에 인스턴스 탐색을 감당할 수 있는 수준으로 유지할 수 있습니다 (Simon Marlow 2010). 제약을 하나씩 완화해 보면, 다음 복잡함이 닥치기 전에 그 제약이 우리에게 무엇을 보장해 주고 있었는지 이해할 기회를 얻게 됩니다.
완전한 꼬리 호출
알려진 호출은 이미 LLVM의 꼬리 호출 규약을 사용합니다. 알려지지 않은 호출은 C 런타임을 거치는데, 그곳에서는 호출이 연쇄되면 여전히 스택 프레임이 쌓일 수 있습니다. 이 간극을 메운다면 꽤 뿌듯할 것입니다.
한 가지 방법은 apply 디스패치도 LLVM으로 생성하는 것입니다. 그러면 정확한 호출, PAP 진입, 과포화(oversaturation)가 하나의 꼬리 호출 프로토콜을 공유할 수 있습니다. 흥미로운 경우는 과포화입니다. 첫 번째 호출이 남은 인자를 받을 함수를 반환하는 동안, 그 남은 인자를 어딘가에 보관해 두어야 합니다. eval/apply 모델이 출발점이 되어 줍니다 (Marlow and Peyton Jones 2004).
특수화와 언박싱된 값
산술 연산의 중간값은 네이티브 레지스터에 둘 수 있지만, 호출할 때는 여전히 균일한 워드를 전달합니다. 컴파일러가 호출 지점에서 구체적인 타입을 알고 있다면, 언박싱된 인자를 받는 특수화된 버전을 만들 수 있을 것입니다. 사전을 알고 있으면 메서드 선택을 직접 호출로 바꿀 수도 있습니다.
이 부분에는 실험해 볼 여지가 많습니다. 실행 파일을 엄청나게 키워 버릴 여지도 많습니다. 특수화된 사본은 코드 공간을 차지하므로, 쓸모 있는 실험이라면 절약한 작업량과 늘어난 코드를 모두 측정해야 합니다. 범용 특수화기를 만들기 전에 작은 수치 함수 하나로 시작해서 결과를 살펴보십시오.
최적화와 디버깅
인라이닝은 다음 패스가 단순화할 수 있는 상수, 생성자, 사전 메서드를 드러낼 수 있습니다. 반대로 작업을 중복시킬 수도 있습니다. 23장에서 다룬 트레이스와 머신 단계 수를 이용하면 이 트레이드오프를 조사할 수 있습니다. 최적화는 우리가 실제로 실행할 수 있는 프로그램에 도움이 됨으로써 제자리를 증명해야 합니다.
소스 디버거도 보람 있는 프로젝트가 될 것입니다. 네이티브 실행 파일이 아래에서 돌아가는 동안 Fun 코드를 한 단계씩 따라가는 모습을 상상해 보십시오. DWARF는 기계 명령어를 소스 위치와 연결할 수 있지만, 스코프와 변수 위치도 기술해야 합니다. 최적화 없이 시작하십시오. 최적화기가 변수를 제거한 뒤 그 변수가 어디로 갔는지 설명하는 일은 나중에 즐겨도 되는 문제입니다.
출발점 고르기
이 아이디어 중 하나를 시도해 보려고 컴파일러 전체를 다시 만들 필요는 없습니다. 각각에는 자연스럽게 시작할 수 있는 지점이 있습니다.
| 방향 | 시작할 곳 |
|---|---|
| 지연 | Core 평가와 클로저 진입 |
| 모듈 | 이름이 바뀐 식별자와 컴파일 단위 메타데이터 |
| 행 또는 이펙트 | 타입, 제약, 정교화 |
| 클래스 확장 | 인스턴스 검사와 제약 풀이 |
| 알 수 없는 호출의 TCO | Flat 호출, 느린 진입점, 런타임 apply |
| 특수화 | 소거 이전의 타입 증거와 호출 규약 |
| 디버깅 | Flat과 LLVM 생성 단계까지 이어지는 소스 스팬 |
세부 사항을 찾아봐야 할 때는 IR 레퍼런스와 런타임 ABI를 참고하십시오.
끝까지 오신 것을 축하합니다! 컴파일러를 만드는 일에는 놀라울 만큼 폭넓은 아이디어가 한데 모이며, 여러분은 소스 텍스트에서 실행되는 프로그램에 이르기까지 그 아이디어들을 따라왔습니다. 한때 위협적으로 보였던 장치 중 일부가 이제는 직접 앉아서 만들 수 있는 것처럼 보이기를 바랍니다.
함수형 프로그래밍은 Haskell 2010 이후로 크게 발전했으며, 우리는 겉만 살짝 훑었을 뿐입니다. 함수형 언어와 타입 시스템, 구현에는 아직 발견할 아름다운 아이디어가 남아 있습니다. 호기심이 생기는 것을 하나 골라 시도해 보십시오. 이것은 여러분의 컴파일러 여정의 시작일 뿐입니다.