그림자 속의 세 버그
해동기는 interpreter로 컴파일해 깨끗하게 돌았고, 그다음 C backend로 세 가지로 컴파일에 실패했다. C 키워드에서 딴 param이 선언과 참조에서 어긋나고. 다른 함수가 같은 이름의 inner helper를 가진 탓에 지역 변수가 capture에서 사라지고. Vec를 반환하는 match 암이 존재하지 않는 struct의 compound literal을 emit. 세 개의 미선언 식별자 에러, 어느 것도 과거 테스트가 닿지 못했다 —— 진짜 프로그램이 그 아래 기구에 드리우는 그림자다.
해동기는 interpreter에서 한 번에 올바로 돌았다. 그다음 C backend에 건네졌고, 컴파일을 거부했다 —— 세 가지로 따로, 어느 것도 생성된 C의 미선언 식별자 에러이고, 어느 것도 closure-lifting 기구의, 어느 테스트도 밀지 않았던 구석에 있었다. interpreter는 그 어느 것도 보지 못했다. 그것이야말로 살아남은 이유다: interpreter는 소스를 직접 평가하지만, 이 버그들은 C로의 번역에 산다. 중첩되고, 재귀하고, capture가 많은 함수로 촘촘한 300줄이, 그것들을 보일 만큼 큰 그림자를 드리운 첫 번째였다.
선언과 사용에서 어긋나는 이름
첫째는 index라는 param이었다. 소스 언어에서는 나무랄 데 없는 이름이지만, index는 C 문자열
라이브러리의 함수이므로, backend는 늘 그런 이름을 모든 참조 지점에서 index_로 sanitize해
왔다. 선언 지점에서는 하지 않았다 —— 그래서 생성된 함수는 param index를 선언하고, body는
index_를 참조했고, 그것은 미선언이었다. 수정은 둘 다 sanitize하는 것, 그리고 그것은 한 곳이
아니라 두 곳에 적용해야 했다: lifted 함수의 param과, 익명 closure adapter의 param —— 깊이
커리화된 inner 함수가 취하는 형태다. 해동기의 Huffman 디코더는 후자가 될 만큼 커리화되어
있었고, 그래서 둘째의, 더 교묘한 경로를 찾았다.
동명이인에게 도둑맞은 변수
둘째가 가장 기묘했다. 그저 지역 변수 p —— let으로 바인딩되고 inner 루프에 읽히는 —— 가,
그 루프의 capture 변수에서 말없이 떨어져, 생성된 호출이 스코프에 없는 p를 참조했다. 원인은
함수를 가로지르는 이름 충돌이다. 컴파일러가 inner 함수를 top-level로 lift할 때, 그 inner
함수의 이름을 capture 값으로 취급해서는 안 된다. 그것을 inner 함수 이름의 global 맵으로
확인하고 있었다 —— 그리고 프로그램의 다른 곳의 다른 함수가, 마침 p라는 inner helper를 갖고
있었다. 그래서 global한 확인은 「p는 inner 함수다, capture에서 제외하라」고 말했고, 지역 변수
p는, 아무 관계 없는 동명이인과 함께 내던져졌다. 수정은 이 확인을 함수마다 스코프한다: 이름은
같은 host의 inner 함수를 가리킬 때만 제외된다. 자유변수 해석은 줄곧 옳았다. 하류의 filter가
그 답을 훔치고 있었던 것이다.
존재하지 않는 타입의 제로 값
셋째는 패턴 매칭에 있었다. 결과 타입이 Vec인 match는, 어느 암도 일치하지 않았을 때 떨어질
값을 요한다(결코 돌지 않지만 타입 검사를 통과해야 하는 abort 경로). 스칼라라면 0, struct라면
{0} compound literal. 코드는 variant 이름의 mangler로 struct 이름을 짜 (Vec___heap_int){0}을
낳았다 —— 하지만 Vec는 C backend에서 값 전달 struct가 아니라 포인터이고, literal을 만들
Vec___heap_int 타입 따위 존재하지 않는다. 포인터 타입 컨테이너는 compound literal이 아니라 null
포인터로 떨어져야 한다. 수정은 컨테이너와 다른 포인터 결과 타입을 0으로 돌린다. 어느 것도
작다. 합쳐서 이 이야기의 교훈이고, dogfooding이 새 각도에서 계속 가르치는 것이다: 기능은 밝고,
테스트는 좁고, 버그는, 진짜 요구가 무거운 프로그램만이 드리울 수 있을 만큼 무거운 그림자 속에
산다. 셋 다 고쳐지고, 해동기는 workaround 없이 컴파일한다 —— 그리고 즉시, 그것들을 무색하게 하는
문제를 드러낸다. 그것이 다음 이야기다: 1메가바이트를 전개하는 데 484메가바이트.