IDE 지원과 증분 컴파일을 염두에 둔 인터랙티브 컴파일러를 중심으로, 실용적인 관점에서 프로그래밍 언어를 구현하는 방법을 단계별로 소개하는 시리즈.
URL: https://thunderseethe.dev/series/making-a-language/
이 시리즈는 프로그래밍 언어를 만드는 방법을 가르칩니다. 이는 교육에서 다루는 컴파일러와 실제 프로덕션에서 보이는 컴파일러 사이의 간극에서 출발했습니다. 프로덕션 컴파일러는 대단히 인터랙티브해야 하고, IDE 사용과 증분 컴파일을 수용하는 데 깊은 관심을 둡니다. 이런 것들이 어떻게 동작하는지 배우려는 과정에서 자료가 매우 부족하다는 것을 겪었습니다. 여기서 구현할 언어는 소박하겠지만, IDE를 지원할 준비가 된 인터랙티브 컴파일러에 필요한 것들에 초점을 맞출 것입니다.
우리의 언어는 이론을 충분히 존중하되, 그 이론을 파고들지는 않을 것입니다. 이론이 내놓은 결과에 기대는 것은 기꺼이 하겠지만, 그 결과를 어떻게 달성하는지까지 해부하진 않습니다. 이 시리즈의 초점은 실용적인 측면에 있습니다. 실행 가능한 코드는 각 섹션과 함께 제공됩니다.
우리 언어의 구현은 여러 패스(pass)로 나눌 수 있습니다: 타입체킹(Typechecking), 로워링(Lowering) 등. 각 패스는 아래에서 하나의 섹션을 가지며, 그 안이 다시 언어의 기능(feature)들로 세분화됩니다. 예를 들어 “타입(Types)”(타입 추론에 관한 첫 번째 패스)은 “Base”, “Rows”, “Items”의 세 가지 기능으로 나뉩니다.
각 기능은 이전 기능 위에 새로운 능력을 층층이 얹습니다. Base는 초기 기능이고, Rows는 Base 언어 위에 행 타입(row types) 지원을 얹으며(그리고 Items는 Rows 위에, 등등) 확장됩니다. 패스/기능 조합들은 서로 연결됩니다. Lowering/Base는 Types/Base에 의존하며, 마지막에는 모든 패스의 Base 기능을 결합해 Base 언어용 컴파일러를 만들 수 있습니다.
각 패스의 기능 묶음을 순서대로 읽어도 되고, 각 기능에 대해 패스별로 읽어도 됩니다. 구현 코드는 동반 저장소에서 확인할 수 있습니다.
Types는 우리 언어의 추상 구문 트리(AST)를 도입하고, AST에 대해 타입을 추론하는 것부터 시작합니다.
Lowering은 타입이 붙은 AST를 중간 표현(IR)으로 변환합니다. 이는 컴파일러의 프런트엔드에서 미들/백엔드로 무게중심을 옮기는 단계입니다.
Simplification은 타입을 보존하는 방식으로 IR을 최적화합니다. 같은 타입의 IR을 더 나은 런타임 성능으로 만들어 냅니다. 이를 달성하는 핵심 재료 중 하나는 인라이닝(inlining)입니다.
Monomorphization은 언어에서 다형성(polymorphism)을 제거하는 역할을 합니다. 다형성을 실행하는 코드를 어떻게 내보낼지 알 수 없기 때문에, 코드 방출을 향한 중요한 단계입니다.
Closure conversion은 IR에서 함수를 제거하고 더 저수준의 구성 요소인 클로저(closure)로 대체합니다. 클로저는 (이전에는 암묵적이었던) 함수의 변수 캡처(variable capture)를 명시적으로 만들어, 컴파일에 더 적합한 형태로 노출합니다.
Code emission(또는 code generation)은 클로저 변환된 항목(item)들을 WebAssembly(Wasm)를 대상으로 실행 가능한 코드로 변환합니다. 이는 트리 구조의 IR을 Wasm 연산 목록으로 평탄화하고, 클로저를 Wasm 구조체(struct)로 변환하는 과정을 포함합니다.
Parsing은 소스 파일이 유효한 문법을 포함하는지 검사하고, 우리 언어의 문법을 표현하는 구체 구문 트리(CST)를 구성합니다.
Desugaring은 구체 구문 트리를 추상 구문 트리로 변환합니다.
Name resolution은 프로그램의 각 이름(name)에 고유한 변수(variable)를 할당합니다.
기본(Base) 언어를 위한 언어 서버(language server)를 구현합니다.