단말에 산다

두 번째 도구는 작은 roguelike —— 맵을 걷고, gold를 줍고, 배회하는 ghost로부터 도망치는 캐릭터. runtime에 없던 한 능력을 강제하기 위해 선택되었다:줄을 기다리지 않고, 키가 눌린 순간에 한 키를 읽기. 그것을 성립시킨 scoping의 결정은, 게임을 turn-based로 유지하는 것으로, 대화적인 프로그램을 키 입력을 흘려 넣어 테스트할 수 있는 것으로 바꿨다 —— CI에서 도는 게임.

meredogfoodterminaltestinglanguage-design

두 번째 도구는 의도해 다른 방향으로 갔다:바깥의 다른 프로그램으로가 아니라, 안으로, 프로그램이 이미 돌고 있는 단말로. 작은 roguelike —— mrog —— 단일한 @가 벽의 맵을 걷고, gold를 줍고, 배회하는 ghost로부터 도망친다. 게임의 무엇도 중요하지 않았다;중요했던 것은, 대화적인 단말 프로그램이 키가 눌린 순간에 한 키를 읽는 힘 없이 존재할 수 없고, 언어는 한 번에 줄 전체만, 사용자가 Enter를 누른 뒤에만 읽을 수 있었다는 것. 그것은 이름이나 수를 읽기엔 훌륭한 방식이고, 캐릭터를 조종하기엔 무용한 방식이다. gap은 raw한 단말 입력이고, 그리고 게임은 그리로 걸어 들어가는 가장 정직한 방식이었다.

gap을 작게 유지하도록 도구를 scoping한다

한 줄 쓰기 전에, 그 뒤의 모든 것을 형성한 설계의 결정이 있었다:게임을 엄격히 turn-based로 유지한다. 키를 누를 때까지 아무것도 움직이지 않는다;ghost는 당신이 한 걸음 나아갈 때에만 한 걸음 나아간다. 이것은 게임으로의 제한으로 들리고, 실제 그렇지만, 그 진짜 목적은 언어 작업을 제한하는 것이었다. 실시간 게임은 non-blocking read, 타이머, 결코 자지 않는 이벤트 루프를 요했을 것이다 —— 능력의 통째로 한 무리, 각각이 자신의 조사다. turn-based 게임은 딱 하나의 새로운 것을 요한다:한 키의 blocking read. 두 번째 gap을 끌어들일 모든 기능을 거부함으로써, 도구는 겨냥을 단일한 과녁에 유지했다. 이것은 그 자체로 이름 붙일 가치가 있는 습관이다:dogfood의 가치는 하나의 빠진 능력을 깔끔하게 강제할 때 가장 높고, 장난감을 scoping하는 것이 교훈을 흐리지 않게 하는 길이다.

raw mode와, 단말을 되돌리는 것

능력 자체는 세 개의 작은 builtin이었다:단말을 raw mode로 하는 것, 그것을 복원하는 것, 한 키를 읽는 것. raw mode는, 키가 줄에 버퍼되는 게 아니라 즉시 도착하는 단말 설정이고, 눌린 키가 스스로를 인쇄 하지 않도록 echo가 꺼진다. 그것이 어떻게 세워졌는가의 두 세부가, 기능 자체보다 중요하다고 판명 되었다. 첫째는 raw mode에서 인터럽트 시그널을 살린 채 남기는 것, 그러면 Control-C가 그저 또 하나의 키 누름으로 삼켜지는 게 아니라, 여전히 처박힌 게임을 멈춘다 —— 단말을 붙잡는 프로그램은 사용자에게 빠져나갈 길을 빚지고 있다. 둘째는, 진짜 단말이 연결되지 않았을 때 —— 입력이 사람이 아니라 파이프 에서 올 때 —— raw mode가 단지 아무것도 하지 않는 것. 그 둘째 선택은 사소한 견고성의 배려로 보인다. 그것이 도구 전체를 테스트 가능하게 만든 것이고, 자신의 섹션에 값한다.

CI에서 도는 게임

raw mode는 입력이 파이프될 때 no-op로 퇴화하므로, 완성된 게임은 통째로 스크립트에서 구동될 수 있었다:「오른쪽, 오른쪽, 오른쪽, 종료」라는 키 입력을 주면, 그 수들을 두고 최종 상태를 인쇄한다. 즉 대화적인 프로그램 —— 존재하는 이유의 전부가 실시간 키보드 입력이었던 것 —— 을 비대화적으로 돌리고, 그 출력을 포착하고, 같은 입력을 돌리는 interpreter에 대해 바이트 단위로 대조할 수 있었다. 게임이 회귀 테스트가 되었다. 세 개의 통조림 시퀀스 —— gold를 모으는 것, 벽으로 걸어 들어가 멈추는 것, 수를 섞는 것 —— 이 interpreter와 native 바이너리에 걸쳐 거동을 고정했다. 음미할 가치가 있는 작은 아이러니다:언어가 가진 가장 완고하게 대화적인 능력을 행사하기 위해 세워진 도구가, 컬렉션 전체 에서 가장 기계적으로 테스트 가능한 프로그램의 하나이기도 하다, 바로 raw한 입력이 사람이 보지 않을 때 사라지도록 설계되었기에.

interpreter만의 일가, 다시

native build는 task runner가 보인 것과 같은 종류의 솔기를 surface했다. 끝 개행 없는 인쇄는 interpreter에서 돌았지만 C backend에는 구현된 적이 없었다;ghost의 배회를 위한 난수 생성도 같았다. 함께 타입 검사를 통과하고, 함께 interpreted로 돌았고, 함께 컴파일되어서야 빠졌다 —— 앞 도구의 빠진 에러 채널이나 파일 검사와 같은 실패의 형태다. 이제 이것은 낯익은 일가였다:interpreter에만 조용히 살며, 그것을 요하는 첫 native 프로그램을 각각 기다리던 한 줌의 작은 runtime 기능. roguelike가 그중 둘을 더 찾았다. 개별 수정에 영리한 것은 아무것도 없다 —— 각각이 backend를 interpreter가 이미 한 것에 잇는 몇 줄이다 —— 하지만 패턴이야말로 실의 결과다:정말로 다른 형태의 native 프로그램을 세우는 것이, 그러지 않았다면 있는 줄도 몰랐을 parity 버그의 저수지를 빼는 길이다.

끝까지 게임은 native 바이너리로서 놀 수 있었다 —— 이동, 충돌, 수집, 그리고 붙잡으면 run을 끝내는 ghost —— 그리고 세 릴리스를 구동했고, 그 하나하나가 이제 모든 프로그램에 속하는 능력이었다. 하지만 더 깊은 수확은 방법에 대한 두 교훈이었다:도구를 빡빡하게 scoping하는 것이 단일한 gap에 겨냥을 유지하고, 기능을 우아하게 퇴화하도록 설계하는 것이 대화적인 프로그램조차 테스트 가능하게 한다, 라는. 다음 도구는 단말도 다른 프로그램도 요하지 않는다. 그것은 raw한 바이트를 읽는다 —— 그리고 그것이 읽기로 선택한 바이트는, 컴파일러 자신이 emit하는 것이었다.

← Back to Mere: 언어를 만들다