호스트 언어를 고르다

새로운 언어는 기존 언어로 작성된다. 그리고 그 선택이, 컴파일러의 어느 부분이 쾌적하고 어느 부분이 끝까지 발목을 잡는지를 조용히 결정한다. 왜 Mere를 OCaml로 구현하는가 —— 컴파일러가 호스트 언어에 실제로 요구하는 것, '검사 가능하게 만든다'는 원칙이 컴파일러 자신에게도 통한다는 것, 그리고 그 결정을 짐작이 아니라 버릴 시도로 내린 이야기.

mereocamlcompilerlanguage-designhost-language

언어는 무에서 만들어지지 않는다. 어떤 다른 언어로 작성되며, 그 호스트가 작업의 어느 부분이 쾌적하고 어느 부분이 끝까지 발목을 잡는지를 조용히 결정한다. Mere를 한 줄 쓰기 전의 첫 진짜 결정은 —— 무엇으로 만들 것인가? 였다.

컴파일러가 호스트 언어에 실제로 요구하는 것

깎아내면, 컴파일러란 트리를 읽고, 여러 번 걸어 다니며, 또 다른 트리나 어떤 텍스트를 만들어 내는 프로그램이다. 구체적인 요구는 분명하다:

  • AST를 표현한다 —— 수십 개의 노드 형태, 일부는 재귀적. 이것에는 대수적 데이터 타입이 필요하다: Int of int, App of expr * expr, 등등.
  • 그것을 빠짐없이 걷는다 —— 모든 패스(평가, 타입 추론, 코드 생성)는 그 형태들에 대한 큰 경우 분석이다. 이것에는 패턴 매칭이 필요하다. 그리고 —— 결정적으로 —— 케이스를 하나 잊었을 때 알려주는 컴파일러가 필요하다.
  • 먼저 트리 워킹 인터프리터 —— 클로저, 환경, 재귀. 이것에는 일급 함수와 GC가 필요하다. 인터프리터가 동시에 메모리 관리 프로젝트가 되지 않도록.
  • 타입 추론 —— 가변 타입 변수를 쓰는 단일화, 일반화, 구체화. 이것에는 값싼 가변 참조나 좋은 영속 자료구조가 필요하다.
  • 진단 —— 모든 노드가 소스 위치를 지니고, 곳곳으로 전달된다.
  • 나중에는 여러 백엔드 —— C, LLVM IR, WebAssembly 텍스트를 내보낸다. 대부분은 규율 있는 문자열 구축이지만, 경우 분석이 늘어나므로 빠짐없음 검사가 다시 값을 한다.

같은 원칙을, 한 단계 위에서

지난 회에서 Mere는 올바름의 검증 가능성 을 최적화한다 —— 프로그램을 읽고 옳다고 알 수 있는 것 —— 고 말했다. 이 가치는 Mere 작성된 프로그램에만 통하는 것이 아니다. Mere 를 위한 컴파일러에도 똑같이 통한다. 컴파일러는 바로, 케이스 누락이나 노드 처리 실수가 실재하면서도 조용한 버그가 되는 종류의 프로그램이다.

그러니 호스트 언어는 그런 버그를 숨기기 어려운 것이어야 한다. 대수적 데이터 타입과 빠짐없는 패턴 매칭을 가진 언어는, “코드 생성기에서 레코드 타입 처리를 잊었다”를 런타임의 기습에서 컴파일 에러로 바꿔 준다. 새 AST 노드를 더하면 불완전한 패스가 모두 켜진다. 호스트 언어 자신의 타입 시스템이 컴파일러를 정직하게 유지하는 도구가 된다 —— Mere가 하는 베팅과 같은 것을, Mere 자신의 구현에 적용하는 셈이다.

왜 OCaml인가

OCaml은 그 형태에 거의 하나하나 맞아떨어진다:

  • 대수적 데이터 타입과 패턴 매칭이 일급이고 편하다 —— AST와 그 패스들이 종이에 스케치하듯 읽힌다.
  • 빠짐없지 않은 매치는 에러로 취급할 수 있는 경고이므로, “모든 노드를 처리했는가?” 검사가 공짜이자 지속적으로 작동한다.
  • GC가 있어 인터프리터를 트리를 걷기만 하는 것으로 만들 수 있다.
  • 성숙한 툴체인(dune, 읽기 쉬운 타입 에러)이 방해하지 않는다.
  • 그리고 ML 계열이다 —— 이것은 이중으로 중요하다. 왜냐하면 Mere 자신이 ML 계열 언어 이기 때문이다. 컴파일러를 쓰는 데 쓰는 관용구가 그 언어가 주제로 삼는 것과 같아서, 직관이 양방향으로 옮겨간다.

그럴듯한 대안도 있다. Rust 같은 시스템 언어라면 더 많은 제어와 속도를 얻지만, 컴파일러는 공유와 변경이 많은 트리·그래프 형태의 코드로 가득하고, 호스트 프로그램 에서 소유권 검사기와 싸우는 것은 잘못된 곳에서 치르는 마찰이다 —— Mere 자신의 명시적 메모리 이야기야말로 이 프로젝트의 핵심이지, 그것을 만드는 동안 한 번 더 치를 것이 아니다. (호스트가 GC를 쓰고, 그것이 만드는 언어는 쓰지 않는 것 —— 이는 모순이 아니라 관심사의 분리다.)

짐작이 아니라, 시도로 결정했다

이 프로젝트에서 반복되는 방법은 “짐작하지 말고, 먼저 작은 시도를 돌린다”다. 호스트 선택은 그것이 처음 적용된 곳이었다. 분위기로 OCaml에 정하는 대신, 결정은 버릴 프로토타입 —— OCaml로 처음부터 끝까지 구현한 작은 식 언어 —— 에서 나왔다. 요구가 까다로운 부분이 모두 정말 자연스러운지 확인하려고 일부러 만든 것이다:

  • ADT AST와 재귀적 평가,
  • 작은 손수 작성한 파서,
  • 클로저와 환경,
  • 곳곳으로 전달되는 소스 위치,
  • 그리고 양방향 타입 검사까지 —— 타입 추론 기계가 어색하지 않을지 확인하려고.

그 프로토타입이 각 부분의 편안함을 확인하고 —— 수십 개의 테스트에 받쳐져 —— 비로소 OCaml을 호스트로 채택했다. 시도는 의도적으로 버릴 것이다. 그 일은 물음에 답하는 것이지, 코드베이스가 되는 것이 아니다.

이것이 이 시리즈에서 이후 이어지는 거의 모든 결정의 형태다. 설계상의 물음을 세우고, 작은 시도나 지상 스케치가 답하고, 그 후에야 비로소 실제 코드가 된다. 호스트 언어는, 마침 그 물음 목록의 처음이었을 뿐이다.

다음 회: 최소 코어 —— 실제로 동작하는 Mere의 최소 버전.

← Back to Mere: 언어를 만들다