interp에서는 도는데

generic한 pairing heap —— 진심으로 세운 언어 첫 다상 데이터 구조 —— 는 타입 검사를 통과하고, interpreter에서 완벽히 돌고, 컴파일되지 않는 C를 낳았다. 추적은 monomorphization의 독립된 두 구멍을 폭로했다: 다상 함수의 body 안에만 존재하는 tuple 형은 한 번도 선언되지 않았고, 하나의 타입으로 해결된 함수는 다른 타입으로도 필요함을 두 번 다시 배울 수 없었다. 둘을 고치자 comparator 구동의 Dijkstra가 모든 backend에서 돌았고 —— 최대의 미결 설계 과제 「타입 클래스 없이 살기」에 대한 정직한 실측이 하나 남았다.

merepolymorphismmonomorphizationcompiler-buglanguage-design

메모리 공사의 단계가 진행되는 사이, 다른 probe가 다른 부채를 재고 있었다. 언어의 ad-hoc 다상은 컴파일 시 특수화다 —— show도 JSON도 구조적 등가도 순서도, 구체 타입마다 derive되고, trait 시스템은 없다 —— 그리고 정직한 물음은, 진짜 generic 컨테이너를 세웠을 때 그것이 어디까지 버티는가였다. probe는 pairing heap: type 'a heap, 순서는 comparator closure, 인스턴스화는 둘 —— 날 정수와 (거리, 노드) 페어 —— 그리고 마무리로 그 위의 Dijkstra. 타입 검사기는 그것을 받아들였다. interpreter는 완벽히 돌렸다. C backend는, 존재하지 않는 타입을 참조하는 코드를 emit했다. 이 갈라짐 —— interpreter에서는 옳고, 바이너리에서는 깨진다 —— 는 이 프로젝트가 아는 가장 배움 많은 실패다. 언어의 의미와 구현이 조용히 괴리했음을 뜻하고, 어디서 괴리했는지는 올바른 형태의 프로그램만이 말할 수 있기 때문이다.

한 번도 선언되지 않은 tuple

첫 구멍은 backend가 무엇을 선언하는가의 쪽에 있었다. C는 프로그램이 쓰는 모든 tuple 형에 struct typedef를 요하고, 그 형을 모으는 collector는 main 식과 모든 함수의 시그니처를 걷고 있었다 —— 하지만 함수의 body는 결코 걷지 않았다. 자기 인자의 페어로 match하는 다상 함수 —— match (h1, h2) with …, 두 heap의 merge를 쓰는 가장 자연스러운 형태 —— 의 그 tuple 타입은, 어느 시그니처에도 나타나지 않는다. body의 주석으로만 존재하고, monomorphize된 instance의 복제 body 안에서 비로소 구체화한다. 그리하여 emit된 C는 tuple_heap_int_heap_int 일족을, 그 struct가 한 번도 선언되지 않은 채 썼고, 빌드는 C 컴파일러에서 떨어졌다. 수정은 정직한 한 문장 길이였다: body도 걸어라. instance의 body는 복제되고 타입 해결이 끝나 있으므로, 걸으면 per-instance의 형이 그대로 얻어진다. 구체성 guard는 다상인 채의 형을 종전대로 건너뛴다.

생각을 바꿀 수 없었던 함수

tuple 선언이 통하자, 둘째의, 더 깊은 구멍이 나타났다. monomorphizer는 다상 함수의 구체적 사용을 스캔으로 찾는다 —— 하지만 스캔 대상은 main 식과 다중 인스턴스 함수의 body뿐이었다. 자신이 단일 타입으로 해결된 함수의 안쪽에 있는 사용은, 불가시였다. 그리하여 main에서 페어 타입으로 나타난 hp_pop은 「단일 해결」되었다: 컴파일러는 원본 skeleton을 그 하나의 arrow와 unify했고 —— 다상성을 파괴했고 —— 이후의 어떤 인스턴스화도 불가능해졌다. int로 해결된 drain이 body 안에서 hp_pop을 int로 부르자, emit된 C는 페어 타입 instance를 int 타입 struct로 불렀다. 수리는 세 부분이었다: 모든 skeleton의 손대지 않은 복제를, 어떤 unification도 닿기 전에 보존한다. 해결된 함수의 body를 발견 스캔에 포함한다. 그리고 해결 끝난 함수에 둘째 타입이 나타나면, 손대지 않은 복제에서 새 spec을 떠서 다중 인스턴스로 승격한다. fixpoint 루프는 반복하는 법을 진작 알고 있었다. 모자랐던 것은, 생각을 바꿀 허가뿐이었다.

영수증으로서의 Dijkstra

두 구멍이 막히자, 영수증은 기분 좋았다: generic heap과, 교과서의 6 노드 그래프 위 Dijkstra가 native로 돌고, interpreter와 바이트 단위로 일치하고, 그 예제는 저장소에 영구 회귀 테스트로 자리 잡았다. probe는 본래 임무의 성과물도 배달했다 —— 타입 클래스 없는 생활의 실측이다. heap을 쓰는 것 자체는 쾌적. 모든 조작에 comparator closure를 통과시키는 것은, 눈에 띄지만 견딜 만한 세금. 정말로 불가능한 것은, derive된 순서를 타입 변수 너머로 쓰는 것이다: fn a -> fn b -> a < b는 int에 고정된다. 특수화에는 특수화할 구체 타입이 필요하기 때문이다. 이것은 이제 억측이 아니라 실측된 비용이고, trait question의 첫 실데이터로서 설계 큐에 앉아 있다. 다만 큐에 앉은 채가 되었다 —— 같은 주, 타입 시스템이, 불편 정도가 아닌 무언가를 허용하고 있던 것이 발견되었기 때문이다.

← Back to Mere: 언어를 만들다