컴파일러는 사람이 쓴 문장을 다른 실행 형식으로 번역한다. 여기서 다룰 작은 컴파일러는 실제 CPU의 모든 기계어를 만드는 도구가 아니라, 앞에서 만든 AST를 바이트코드로 바꾸는 번역기다.
핵심은 문장의 뜻을 새로 결정하는 데 있지 않다. 파서가 만든 구조가 가진 의미를 잃지 않고, VM이 실행할 수 있는 명령어 목록으로 옮기는 데 있다.
AST는 어떻게 바이트코드가 될까
1 + 2 * 3은 토크나이저와 파서를 거쳐 AST가 된다. AST에는 곱셈이 덧셈보다 먼저 계산돼야 한다는 구조가 이미 담겨 있다.
(+) PUSH 1
/ \ PUSH 2
1 (*) → PUSH 3
/ \ MUL
2 3 ADD
컴파일러는 트리의 왼쪽과 오른쪽을 어떤 순서로 명령어로 펼칠지 정한다. 이때 문법을 다시 문자 단위로 읽지 않는다. 문법을 읽는 일은 파서가, 실행 순서를 명령어로 내보내는 일은 컴파일러가 맡는다.
중간 표현을 두면 무엇이 분리될까
바이트코드는 중간 표현이다. 소스 문법과 실행기를 바로 붙이지 않고 중간 단계를 두면, 파서·컴파일러·VM이 각자 다른 일을 할 수 있다.
소스 코드
→ 토큰
→ AST
→ 바이트코드
→ VM 실행
파서는 문법 구조를 만들고, 컴파일러는 그 구조를 명령어로 바꾸며, VM은 명령어를 실행한다. 예를 들어 VM은 원래 소스 코드가 어떤 문법이었는지 몰라도 자기 명령어만 정확히 실행하면 된다.
변수 이름은 저장 위치로 바뀔 수 있다
처음에는 LOAD total, STORE total처럼 이름을 명령어에 남겨도 된다. 더 단순한 실행 형식으로 만들고 싶다면 컴파일러가 변수 이름을 슬롯 번호로 바꿀 수 있다.
let total = 4 + 5
total * 2
PUSH 4
PUSH 5
ADD
STORE 0
LOAD 0
PUSH 2
MUL
total이라는 이름은 컴파일 단계에서 0번 저장 칸으로 연결된다. VM은 문자열 이름을 찾는 대신 0번 칸을 읽는다. 함수와 중첩 범위가 생기면 같은 이름이 어느 저장 칸을 뜻하는지 정하는 규칙이 더 중요해진다.
컴파일 오류와 실행 오류는 다르다
선언되지 않은 이름을 사용하거나 문법이 맞지 않는 문제는 실행 전에 찾을 수 있다. 예를 들어 total을 선언하기 전에 사용했다면 바이트코드를 만들기 전에 멈추게 할 수 있다.
하지만 10 / 0처럼 실제 값이 있어야 알 수 있는 문제는 실행 중에 나타날 수 있다. 컴파일러가 있다고 모든 오류를 미리 없애는 것은 아니다. 어떤 오류를 어느 단계가 책임질지 구분하는 것이 먼저다.
컴파일한 결과를 누가 실행할까
바이트코드로 번역한 결과는 VM이 실행한다. 이것도 컴파일의 한 형태다. 반면 C나 Rust 같은 언어의 컴파일러는 운영체제와 CPU가 실행할 기계어 또는 그에 가까운 형식을 만들 수 있다.
둘의 공통점은 소스의 구조를 다른 실행 형식으로 옮긴다는 것이다. 차이는 최종 결과를 누가 실행하는가에 있다. 작은 컴파일러를 이해해 두면 기계어, JIT, 최적화도 같은 흐름에서 볼 수 있다.
이해 확인 질문과 답변
컴파일러와 인터프리터의 차이는 무엇일까?
인터프리터는 AST 같은 구조를 따라가며 바로 값을 만든다. 컴파일러는 그 구조를 먼저 바이트코드나 기계어처럼 다른 형식으로 번역한다. 작은 시스템에서는 구조를 바로 실행하는가, 실행 전에 명령어로 옮기는가로 구분해 보면 된다.
중간 표현을 두는 이유는 무엇일까?
소스 문법을 읽는 일과 실행하는 일을 분리하기 위해서다. 파서는 AST를 만들고, 컴파일러는 AST를 중간 표현으로 바꾸며, VM은 중간 표현을 실행한다. 각 단계의 입력과 출력이 분명해지므로 규칙을 바꾸거나 오류를 찾을 위치도 선명해진다.
컴파일러가 있으면 모든 오류를 실행 전에 찾을 수 있을까?
아니다. 문법 오류나 선언되지 않은 이름처럼 코드만 보고 알 수 있는 문제는 미리 찾을 수 있다. 하지만 나누는 값이 실행 시점에 0이 되는 경우처럼 실제 데이터가 필요한 문제는 실행 중에 확인해야 한다.
바이트코드를 만드는 것도 컴파일일까?
그렇다. 컴파일의 핵심은 사람이 쓴 고수준 구조를 다른 실행 형식으로 번역하는 데 있다. 그 결과를 VM이 실행하든 CPU가 실행하든, 의미를 보존하는 변환 규칙이 필요하다는 점은 같다.
AI에게 이렇게 요청할 수 있다
숫자식과 변수 선언을 받는 작은 컴파일러를 설명해 줘. 토큰, AST, 바이트코드가 각각 무엇을 맡는지 구분하고 let total = 4 + 5; total * 2가 STORE 0, LOAD 0을 포함한 명령어 목록으로 바뀌는 과정을 보여 줘. 선언 전 변수 사용과 0으로 나누기는 어느 단계에서 발견되는지도 설명해 줘.
작은 컴파일러의 핵심은 소스 코드 → AST → 실행 형식으로 의미를 잃지 않고 옮기는 규칙이다. 다음부터는 실행할 프로그램이 메모리를 어떻게 쓰고, 여러 작업과 통신을 어떻게 안전하게 나누는지로 학습 지도를 넓힌다.
'배움과 성장 > 알고리즘·문제풀이' 카테고리의 다른 글
| 작은 바이트코드 VM은 왜 AST 대신 명령어를 실행할까? 스택과 명령어 포인터 원리 (0) | 2026.08.25 |
|---|---|
| 작은 인터프리터는 문장을 어떻게 실행할까? 토큰·파서·AST 원리 (0) | 2026.08.24 |
| 해시 테이블은 왜 빠를까? 충돌·삭제·리사이징으로 이해하는 원리 (0) | 2026.08.13 |
| 백준 19644번: 좀비 떼가 기관총 진지에도 오다니 문제풀이 (0) | 2024.12.07 |
| 백준 20186 수 고르기 Python: K개 최댓값과 등차수열 벌점 (2) | 2024.12.06 |
댓글