다르게 아프도록 선택된 두 번째 dogfood
Mere로 세운 첫 애플리케이션은 Wasm-and-Node 호스트 위의 web 앱으로, 모든 시스템 콜이 그것에 공급되었다. 두 번째는, 그 편안함을 없애도록 의도해 선택되었다: native 바이너리로 컴파일되는 jq 풍의 커맨드라인 도구로, web 호스트가 늘 그것을 대신해 답해 온 물음에 C backend가 답하도록 강요했다 —— 인자를 어떻게 읽는가, 표준 입력을 어떻게 읽는가, exit 코드를 어떻게 세우는가, 그리고 가장 날카롭게, 두 라이브러리를 내부를 충돌시키지 않고 어떻게 합성하는가. 도구는 실이었지만, friction log가 바로 노림수였다.
언어에 무엇이 빠졌는지를 찾는 방식은, 그것으로 무언가를 세우고, 어디서 저항하는지를 보는 것이다. Mere로 세운 첫 실 애플리케이션은 작은 공동 편집 노트 서버로, Node 호스트 아래에서 WebAssembly로 돌며, 많은 것을 행사했다 —— 하지만 호스트는 관대했다. 프로그램이 필요로 하는 모든 시스템 콜을, Node runtime이 공급했다: 파일, 소켓, 타이머, 표준 출력. 통째로 한 부류의 물음이 결코 나타나지 않았다, 무언가 다른 것이 그것에 답하고 있었기에.
그래서 두 번째 애플리케이션은, 무엇을 낳을까가 아니라, 어디서 아플까로 선택되었다. JSON과 CSV를
커맨드라인에서 질의하는 jq 풍의 도구, C backend를 통해 native 바이너리로 컴파일되는 그것은,
web 호스트가 숨겨 온 영역에 꼭 착지한다: 환경적 runtime 없음, 인자와 표준 입력과 exit 코드를 가진
실의 프로세스, 그리고 관대한 호스트에 로드되는 게 아니라 함께 컴파일되어야 하는 라이브러리. 도구
자신은 유용하다. 하지만 그 friction log —— 언어가 밀쳐낸 모든 곳의 흐르는 기록 —— 이 바이너리와
똑같이 성과물이었다.
프로세스 가장자리의 벽
첫 저항은 즉각적이고 전면적이었다. 커맨드라인 도구의 목적 그 자체는 인자와 입력을 읽는 것이고,
interpreter에서는 이 모두가 돌았다: 인자 벡터를 얻고, 표준 입력에서 한 줄 읽고, 상태 코드로 exit
하는 builtin이 있었다. native 바이너리로 컴파일하면, 그 어느 것도 존재하지 않았다. C backend는
args와 read_line을 마치 함수인 것처럼 참조하는 코드를 emit했지만, 결코 정의하지 않았고, C
컴파일러는 미정의 식별자로 멈췄다. native한 Mere 프로그램은, 그 순간, 하나의 하드코딩된 파일 경로를
읽을 수 있을 뿐, 그 외에는 아무것도 할 수 없었다 —— 인자도, 파이프도, exit 상태도 없이.
이것은 관대한 호스트가 숨기는 gap의 바로 그 형태다. web 호스트에서는, 인자 전달과 입력은 호스트의
일이었다; 언어는 그것들을 실의 오퍼레이팅 시스템 프로세스로 lower하도록 요구받은 적이 없었고,
그래서 결코 하지 않았다. gap을 닫는 것은, 일단 보이면 극적이지 않았다: 인자 벡터가 먼저 왔기에,
main은 프로세스의 argc와 argv를 취해 나머지를 프로그램에 넘겼다; 다음으로 표준 입력 전체를
읽는 것, 그래서 도구가 파이프의 끝에 앉을 수 있고; 다음으로 실의 프로세스 exit, 도달 불능 에러가
이미 그러했던 것과 같은 방식으로 C 라이브러리의 exit로 lower된다. 저마다 자기 릴리스에 착지했다.
어느 것도 어렵지 않았다. 요점은, 프로그램이 흔한 Unix 시민이 되려 하기 전까지, 어느 것도 존재할
이유가 없었다 —— 그리고 그때, 셋 다 편의 기능이 아니라 전제 조건이었다, 는 것이다.
섀도잉을 잊은 backend
다음 frictions의 일가는 모두 하나의 형태를 가졌다: C backend가, 프로그램이 그 이름을 rebind했는지를
먼저 확인하지 않고, 이름을 builtin으로 다룬다. 병행 처리의 작업이 join builtin을 더했었다 ——
spawn된 스레드를 기다리는 것 —— 그리고 CSV 라이브러리는, 아주 이치에 맞게, 문자열을 이어 붙이는
자기 자신의 로컬 join을 가지고 있었다. 컴파일하면, 문자열 joiner는 스레드 primitive로의 호출로
바뀌었고, C 컴파일러는 문자열에는 기다릴 스레드 핸들이 없다고 불평했다. dispatch는, 이 특정 join이
builtin인지 그 앞에 선 로컬 정의인지를 묻기 전에, 이름 join에 match했다.
interpreter와 타입 검사기는 늘 그 섀도잉을 존중했다; C backend는 그러지 않는 별개의 dispatch 경로를 가지고 있었다. 같은 이야기가 더 작은 형태로 반복되었다 —— interpreter와 Wasm backend에는 존재하나 C에는 없는 문자열 등가 함수, 수치에서는 타입 검사가 통과하나 문자열에서는 통과하지 않는 순서 비교. 저마다가, 한 backend의 coverage가 다른 것들로부터 조용히 어긋난 곳이고, 저마다가 native 빌드가 거기에 걸어 들어갈 때까지 보이지 않았다. 네 backend의 언어는, 그 parity를 바로 이 통화로 지불한다: gap은 실이고, 개별로는 작고, 한 번에 하나의 dogfood로 표면화한다.
가장 날카로운 발견: 충돌한 두 라이브러리
연습 전체의 최상의 발견은 두 라이브러리를 동시에 요했다. JSON 배열의 파싱은 돌았다. 그 곁에 CSV 라이브러리를 import해 같은 배열을 파싱하면 컴파일에 실패했다, 쓰이나 결코 선언되지 않는 변수로 —— 사라져 버린, 포착된 문자열 길이. 어느 라이브러리도 단독으로는 틀리지 않았다; 조합만이 깨졌고, 그것은 어느 부품도 아니라 그것들을 합성하는 기구의 문제의 서명이다.
원인은 사냥할 값이 있었다. backend가 안쪽의 재귀 함수를 최상위로 lift할 때, 그 함수가 어느 바깥의
변수를 포착했는지를 알아내야 하고, 그것은 lift된 함수의 이름을 단일한 글로벌 테이블을 통해 해결하고
있었다. 두 라이브러리가 우연히 loop라는 이름의 안쪽 헬퍼를 담고 있었다. JSON 파서의 loop는
자기 자신을 재귀적으로 불렀다 —— 그리고 그 자기 호출이, 글로벌 테이블을 통해 해결되어, CSV
라이브러리의 lift된 loop에 대신 착지했고, 그래서 JSON의 loop는 CSV의 loop가 포착한 변수를 물려받고,
그 자신의 호출자는 들은 적도 없었다. 두 다른 파일의 두 동명 로컬이, 소스에서는 서로 보이지 않는데,
컴파일러에 조용히 merge되어 있었다. 수정은, 그 이름들을 글로벌이 아니라 host 함수마다 해결하는
것으로, 그래서 loop라는 이름의 두 안쪽 함수는 별개로 남는다 —— 그리고 그것은 최소 재현, 저마다
loop를 가진 두 함수를, 회귀 테스트로 지니고 왔다. native 라이브러리 합성의 실의 한계, 둘이 마침내
하나의 바이너리를 공유하도록 요구받았기에 비로소 찾아졌다.
언어가 저항하지 않은 곳
friction만을 제시하는 것은 왜곡일 것이다, log의 가장 유용한 엔트리 중 하나가 그 부재를 기록하기 때문이다. 도중에, 쿼리 엔진이 하나의 JSON 값을 하나로 map하는 함수에서, 셀렉터가 값을 여럿으로 폭발시키고 셀렉터가 sequence될 수 있는 streaming 모델로 재설계되었다 —— 도구의 코어로의 비자명한 변경이다. 그것은 Mere로 아무 저항 없이 쓰였다: 셀렉터 타입의 새 case, 그리고 나머지는 평범한 재귀. 이것은 지금까지의 모든 dogfood가 말해 온 것에 맞는다. 언어의 코어 —— 대수적 데이터 타입, 패턴 매칭, 나무와 스트림 위의 재귀 —— 는 작업이 편안한 곳이다. 아픔은 가장자리에 모인다: native한 입력과 출력, 라이브러리의 패키징, 문자열과 Unicode의 구석. 한가운데는 버틴다.
log가 루프였다
그 모두를 꿰는 실은 루프이고, 루프가 방법이다. 실의 프로그램이 저항할 때까지 세워진다; 저항이 정밀한 signal로 적힌다; 날카로운 것이 언어 자신 안의 수정이 된다; 그리고 프로그램이 그 수정을 채용하고, 지니고 온 workaround를 떨군다. 도구는, 아직 컴파일할 수 없는 CSV 라이브러리를 대신해 선 수제의 문자열 스플리터로 시작했다; 합성 버그가 수정되자, 그 workaround는 삭제되고 도구는 실의 라이브러리를 import해, 제대로 된 따옴표 필드 파싱을 공짜로 얻었다. 바이트 값으로 쓰인 문자 분류 루틴은, 그것들이 타입 검사를 통과하게 되자, 되고 싶었던 직접의 문자열 비교로 다시 쓰였다. workaround는 흉터가 아니다; 닫힌 루프의 영수증이다. 다르게 아파하는 도메인을 선택하는 것은 비관이 아니었다. 언어가 다음에 필요로 하는 실의 것을 찾고, 그런 뒤 세우는, 최속의 방식이었다.