LabHub
Get started
배우기 러닝패스 코스

Compilers — Build a Small Language from Start to Finish

A Program That Translates and a Program That Runs

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

컴파일러는 프로그램을 실행하지 않고 다른 언어의 프로그램으로 옮기는 프로그램이고, 인터프리터는 프로그램을 읽으면서 바로 실행하는 프로그램입니다. 둘은 같은 앞단(글자를 토큰으로, 토큰을 트리로, 트리의 뜻을 검사)을 공유하고, 마지막에 '지금 계산할 것인가, 나중에 계산할 코드를 남길 것인가' 에서 갈립니다.

왜 이게 필요했나

CPU 는 x = (a + 1) * 2 를 읽지 못합니다. 읽을 수 있는 것은 add, imul 같은 명령과 레지스터 번호뿐입니다. 사람이 쓰기 좋은 글과 기계가 돌릴 수 있는 명령 사이의 거리를 누군가 메워야 하는데, 메우는 시점이 두 가지입니다.

실무에서 둘은 섞여 있습니다. 파이썬은 소스를 바이트코드로 컴파일한 뒤(__pycache__.pyc) 그 바이트코드를 인터프리트합니다. 자바는 javac 가 바이트코드를 만들고, JVM 이 처음엔 인터프리트하다가 자주 도는 메서드만 기계어로 컴파일합니다(JIT). 그래서 "이 언어는 컴파일 언어인가" 보다 "어느 단계를 언제 하는가" 가 더 정확한 질문입니다.

어떻게 동작하나

컴파일러는 한 덩어리가 아니라 단계의 줄입니다. 앞 단계의 출력이 다음 단계의 입력이 되고, 이 코스의 모듈도 그 줄을 그대로 따라갑니다.

소스 글자 ──렉서──▶ 토큰 ──파서──▶ 트리(AST) ──의미 분석──▶ 검사된 트리
   (2모듈)            (3모듈)              (4모듈)
                                              │
            ┌─────────────────────────────────┼──────────────────────────┐
            ▼                                 ▼                          ▼
     트리를 걸으며 실행               바이트코드 + 스택 VM        중간 표현 → 최적화 → x86-64
        (5모듈)                            (6모듈)                  (7·8·9모듈)

gcc 도 같은 줄을 몇 개의 프로그램으로 나눠 돌립니다. 평소에는 gcc hello.c 한 줄에 가려 보이지 않지만, 옵션으로 단계마다 멈출 수 있습니다.

멈추는 옵션 한 일 산출물
-E 전처리 — #include 를 펼치고 #define 을 바꿔 넣는다 .i(아직 C)
-S 컴파일 — C 를 어셈블리로 .s(글자)
-c 어셈블 — 어셈블리를 기계어로 .o(재배치 가능 목적 파일)
(없음) 링크 — 목적 파일과 라이브러리를 이어 붙인다 실행 파일

목적 파일에는 아직 주소가 정해지지 않은 이름이 남습니다. hello.o 가 부르는 printf 는 libc 에 있으므로, nm hello.o 는 그 이름 앞에 U(undefined)를 찍습니다. 링커가 그 빈칸을 채웁니다. 링크 단계에서만 나는 오류("undefined reference to …")가 따로 있는 이유가 이것입니다 — 컴파일은 파일 하나만 보고, 링크는 전부를 봅니다.

인터프리터와 컴파일러가 같은 뜻을 지키는지도 따로 확인해야 합니다. 이 실습의 계산기는 나눗셈을 C 처럼 0 쪽으로 자릅니다(-7 / 2 는 -3). 파이썬의 // 는 아래로 내리므로(-4) 인터프리터를 파이썬으로 짜면서 // 를 쓰면, 같은 프로그램이 인터프리터와 컴파일한 실행 파일에서 다른 답을 냅니다. 두 구현의 뜻이 같은지는 저절로 지켜지지 않고, 돌려서 대조해야만 압니다. 이 코스의 채점기가 거의 모든 단계에서 기준 구현과 무작위 입력으로 대조하는 이유도 같습니다.

현장에서 만나는 모습

이 코스 내내 만들 언어는 '미니' 입니다. 64비트 정수와 참거짓, let·print·if·while·fn·return, 그리고 우선순위가 있는 연산자 스무 개 남짓이 전부입니다. 작지만 렉서부터 x86-64 코드까지 한 줄로 이어 보기에 모자라지 않고, 모든 단계를 채점기가 실제로 돌려 볼 수 있습니다.

다음 실습에서 할 것

/opt/fixtures/mini/c/hello.c 를 gcc 로 단계마다 멈춰 네 산출물을 만들고, nm 으로 목적 파일의 빈칸을 봅니다. 그다음 RPN 계산기를 rpn.py 하나에 두 번 만듭니다 — 토큰으로 자르고, 스택으로 바로 계산하는 인터프리터, 실행하지 않고 스택 깊이만 따라가는 검사기, 같은 계산을 하는 C 를 써서 gcc 로 굽는 컴파일러. 끝으로 두 길이 같은 답을 내는지 무작위 프로그램으로 대조하고, 어느 쪽이 언제 빠른지 잽니다.