- Published on
Compilers and Interpreters: How Code Actually Runs — Everything Developers Need to Know
- Authors

- Name
- Youngju Kim
- @fjvbn20031
1. Why Understanding Compilation Matters
Behind the simple fact that "you write code, it runs" lies decades of language processing technology. Understanding how compilers and interpreters work provides real practical benefits.
Performance intuition — You will understand why V8 Hidden Classes matter, why Python is slow, and what JVM warm-up means.
Debugging ability — Knowing that "SyntaxError: Unexpected token" comes from the parser stage speeds up debugging dramatically.
Tool mastery — Babel, TypeScript, ESLint, and Prettier all use compiler technology (lexers, parsers, ASTs). Understanding the principles lets you build custom plugins.
Interview preparation — This is a frequently tested topic at FAANG and major tech companies.
2. The Compilation Pipeline: A Bird's Eye View
Let us walk through the stages that transform source code into an executable program.
Source Code → [Lexer] → Tokens → [Parser] → AST → [Semantic Analysis] → IR → [Optimization] → [Code Gen] → Execution
| Stage | Input | Output | Key Role |
|---|---|---|---|
| Lexing | Source string | Token stream | Split text into meaningful units |
| Parsing | Token stream | AST | Build a tree of grammatical structure |
| Semantic Analysis | AST | Type-checked AST | Type checking, scope resolution |
| IR Generation | AST | Intermediate Representation | Platform-independent intermediate code |
| Optimization | IR | Optimized IR | Constant folding, inlining, etc. |
| Code Generation | Optimized IR | Machine code / Bytecode | Generate target platform code |
This pipeline is shared across nearly every language processing system: GCC, LLVM, V8, JVM, and more. Let us examine each stage in depth.
3. Lexer / Tokenizer
The lexer splits a source code string into tokens — meaningful units. It removes whitespace and comments, and distinguishes keywords, identifiers, literals, and operators.
Token Types
# Input: "let x = 42 + y;"
# Output tokens:
# [LET, "let"] [IDENT, "x"] [ASSIGN, "="] [NUMBER, "42"]
# [PLUS, "+"] [IDENT, "y"] [SEMICOLON, ";"]
A Simple Python Lexer
import re
from enum import Enum, auto
from dataclasses import dataclass
from typing import List
class TokenType(Enum):
NUMBER = auto()
PLUS = auto()
MINUS = auto()
STAR = auto()
SLASH = auto()
LPAREN = auto()
RPAREN = auto()
IDENT = auto()
ASSIGN = auto()
LET = auto()
EOF = auto()
@dataclass
class Token:
type: TokenType
value: str
line: int
col: int
KEYWORDS = {"let": TokenType.LET}
TOKEN_PATTERNS = [
(r'\d+(\.\d+)?', TokenType.NUMBER),
(r'[a-zA-Z_]\w*', TokenType.IDENT),
(r'\+', TokenType.PLUS),
(r'-', TokenType.MINUS),
(r'\*', TokenType.STAR),
(r'/', TokenType.SLASH),
(r'\(', TokenType.LPAREN),
(r'\)', TokenType.RPAREN),
(r'=', TokenType.ASSIGN),
]
def tokenize(source: str) -> List[Token]:
tokens = []
pos = 0
line = 1
col = 1
while pos < len(source):
if source[pos] in ' \t':
pos += 1
col += 1
continue
if source[pos] == '\n':
pos += 1
line += 1
col = 1
continue
matched = False
for pattern, token_type in TOKEN_PATTERNS:
regex = re.compile(pattern)
match = regex.match(source, pos)
if match:
value = match.group()
actual_type = KEYWORDS.get(value, token_type)
tokens.append(Token(actual_type, value, line, col))
pos = match.end()
col += len(value)
matched = True
break
if not matched:
raise SyntaxError(
f"Unexpected char '{source[pos]}' at {line}:{col}"
)
tokens.append(Token(TokenType.EOF, "", line, col))
return tokens
# Test
source = "let x = 42 + y"
for tok in tokenize(source):
print(f"{tok.type.name:8} | {tok.value}")
Output:
LET | let
IDENT | x
ASSIGN | =
NUMBER | 42
PLUS | +
IDENT | y
EOF |
Production lexers handle string literals, comments, Unicode, and much more, but the core principle remains the same.
4. Parser and AST (Abstract Syntax Tree)
The parser takes a token stream and produces an AST (Abstract Syntax Tree) — a tree representation of the code's logical structure.
Recursive Descent Parser
The most intuitive parsing technique, where each grammar rule becomes a function.
@dataclass
class NumberNode:
value: float
@dataclass
class BinaryOpNode:
left: any
op: str
right: any
@dataclass
class AssignNode:
name: str
value: any
class Parser:
def __init__(self, tokens: List[Token]):
self.tokens = tokens
self.pos = 0
def current(self) -> Token:
return self.tokens[self.pos]
def eat(self, token_type: TokenType) -> Token:
token = self.current()
if token.type != token_type:
raise SyntaxError(
f"Expected {token_type}, got {token.type}"
)
self.pos += 1
return token
def parse_expression(self):
left = self.parse_term()
while self.current().type in (TokenType.PLUS, TokenType.MINUS):
op = self.eat(self.current().type).value
right = self.parse_term()
left = BinaryOpNode(left, op, right)
return left
def parse_term(self):
left = self.parse_factor()
while self.current().type in (TokenType.STAR, TokenType.SLASH):
op = self.eat(self.current().type).value
right = self.parse_factor()
left = BinaryOpNode(left, op, right)
return left
def parse_factor(self):
token = self.current()
if token.type == TokenType.NUMBER:
self.eat(TokenType.NUMBER)
return NumberNode(float(token.value))
elif token.type == TokenType.LPAREN:
self.eat(TokenType.LPAREN)
node = self.parse_expression()
self.eat(TokenType.RPAREN)
return node
elif token.type == TokenType.IDENT:
self.eat(TokenType.IDENT)
return token.value
raise SyntaxError(f"Unexpected token: {token}")
AST Visualization
The AST for let x = 42 + y * 2:
AssignNode
├── name: "x"
└── value: BinaryOpNode(+)
├── NumberNode(42)
└── BinaryOpNode(*)
├── "y"
└── NumberNode(2)
Pratt Parsing (Operator Precedence Parsing)
Pratt parsers handle operator precedence elegantly by assigning a binding power to each token.
PRECEDENCE = {
TokenType.PLUS: 10,
TokenType.MINUS: 10,
TokenType.STAR: 20,
TokenType.SLASH: 20,
}
def pratt_parse(parser, min_bp=0):
left = parser.parse_factor()
while True:
op = parser.current()
bp = PRECEDENCE.get(op.type, 0)
if bp <= min_bp:
break
parser.eat(op.type)
right = pratt_parse(parser, bp)
left = BinaryOpNode(left, op.value, right)
return left
Pratt parsing is used in production parsers like the Rust compiler, ESLint, and TypeScript.
5. Compiler vs Interpreter vs JIT
Comparison Table
| Feature | Pure Compiler | Pure Interpreter | JIT Compiler |
|---|---|---|---|
| Examples | C, C++, Rust, Go | Shell script, early BASIC | JavaScript (V8), Java (HotSpot) |
| Pre-execution | Full compilation required | None | Bytecode compilation |
| Execution speed | Very fast | Slow | Fast (after warm-up) |
| Startup time | Slow (compile time) | Fast | Medium |
| Memory usage | Low | Medium | High (includes compiler) |
| Debugging | Harder | Easier | Medium |
| Optimization level | Static analysis | None | Runtime profiling-based |
The Hybrid Reality
Most modern language implementations use a hybrid approach:
- JavaScript (V8): Parse → Bytecode (Ignition) → JIT (TurboFan)
- Python (CPython): Parse → Bytecode → Interpreter (PyPy adds JIT)
- Java (HotSpot): javac compile → Bytecode → Interpreter + C1/C2 JIT
- C# (.NET): Roslyn compile → IL → JIT (RyuJIT)
Purely compiled or purely interpreted languages are rare in practice.
6. V8 Engine: How JavaScript Executes
Google's V8 engine powers both Chrome and Node.js. Let us see how it makes JavaScript fast.
V8 Pipeline
JavaScript Source
↓
Parser → AST
↓
Ignition (Bytecode Interpreter)
↓ (Collects profiling data)
TurboFan (Optimizing JIT Compiler)
↓
Optimized Machine Code
↓ (Deoptimization — if assumptions break)
Fall back to Ignition
Hidden Classes (Shapes)
V8 assigns Hidden Classes (also called Shapes or Maps) to JavaScript objects to optimize property access.
// Good pattern — same Hidden Class shared
function Point(x, y) {
this.x = x; // Hidden Class C0 → C1
this.y = y; // Hidden Class C1 → C2
}
const p1 = new Point(1, 2); // Shape: C2
const p2 = new Point(3, 4); // Shape: C2 (same!)
// Bad pattern — Hidden Class mismatch
const a = {};
a.x = 1;
a.y = 2; // Shape: S1
const b = {};
b.y = 2; // Different order!
b.x = 1; // Shape: S2 (different Shape!)
Always add properties in the same order so V8 can reuse Hidden Classes.
Inline Caches
V8 maintains a cache at every property access site.
function getX(obj) {
return obj.x; // Inline Cache created here
}
// Monomorphic (1 Shape) — fastest
getX(p1); // Cached for Shape C2
getX(p2); // Shape C2 — cache hit!
// Polymorphic (2-4 Shapes) — slightly slower
// Megamorphic (5+ Shapes) — slow, generic path
V8 Optimization Tips
- Always initialize object properties in the same order
- Keep argument types consistent for functions
- Do not mix types in arrays (no strings in number arrays)
- Use
obj.prop = undefinedinstead ofdelete obj.prop - Minimize try-catch blocks; extract them into separate functions
7. CPython: How Python Executes
CPython Bytecode
CPython compiles source code to bytecode, then executes it on a virtual machine.
import dis
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# View bytecode
dis.dis(fibonacci)
Simplified output:
2 0 LOAD_FAST 0 (n)
2 LOAD_CONST 1 (1)
4 COMPARE_OP 1 (<=)
6 POP_JUMP_IF_FALSE 12
3 8 LOAD_FAST 0 (n)
10 RETURN_VALUE
4 >> 12 LOAD_GLOBAL 0 (fibonacci)
14 LOAD_FAST 0 (n)
16 LOAD_CONST 2 (1)
18 BINARY_SUBTRACT
20 CALL_FUNCTION 1
...
GIL (Global Interpreter Lock)
CPython's GIL ensures only one thread can execute Python bytecode at a time.
# GIL makes multithreading inefficient for CPU-bound tasks
import threading
import time
def cpu_bound(n):
count = 0
for i in range(n):
count += i
return count
# Single thread
start = time.time()
cpu_bound(50_000_000)
cpu_bound(50_000_000)
print(f"Single: {time.time() - start:.2f}s")
# Multi-thread (can be slower due to GIL)
start = time.time()
t1 = threading.Thread(target=cpu_bound, args=(50_000_000,))
t2 = threading.Thread(target=cpu_bound, args=(50_000_000,))
t1.start(); t2.start()
t1.join(); t2.join()
print(f"Multi: {time.time() - start:.2f}s")
# Solution: use multiprocessing
from multiprocessing import Pool
start = time.time()
with Pool(2) as p:
p.map(cpu_bound, [50_000_000, 50_000_000])
print(f"Multi-process: {time.time() - start:.2f}s")
PyPy: The JIT Alternative for Python
PyPy is a Python interpreter written in RPython with JIT compilation support.
CPython: fibonacci(35) → ~4.5 seconds
PyPy: fibonacci(35) → ~0.3 seconds (roughly 15x faster)
Why PyPy is faster:
- Tracing JIT — compiles hot loops to machine code
- Guards — falls back to interpreter if type assumptions break
- GIL still exists — but STM (Software Transactional Memory) experiments continue
8. JVM: How Java Executes
From javac to Bytecode
// Hello.java
public class Hello {
public static void main(String[] args) {
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i;
}
System.out.println(sum);
}
}
# Compile
javac Hello.java
# View bytecode
javap -c Hello.class
public static void main(java.lang.String[]);
Code:
0: iconst_0
1: istore_1 // sum = 0
2: iconst_0
3: istore_2 // i = 0
4: iload_2
5: sipush 1000
8: if_icmpge 21 // if i >= 1000 goto 21
11: iload_1
12: iload_2
13: iadd
14: istore_1 // sum += i
15: iinc 2, 1 // i++
18: goto 4
21: ... // println
HotSpot JIT: C1 and C2 Compilers
Bytecode Execution (Interpreter)
↓ (Method invocation counter)
C1 Compiler (Fast compilation, basic optimizations)
↓ (More executions)
C2 Compiler (Slow compilation, aggressive optimizations)
↓
Optimized Machine Code
| Feature | C1 (Client) | C2 (Server) |
|---|---|---|
| Compile speed | Fast | Slow |
| Optimization level | Basic | Aggressive |
| Inlining | Limited | Aggressive |
| Loop optimization | Basic | Unrolling, vectorization |
| When used | Early | Hot methods |
GraalVM: The Polyglot Runtime
GraalVM can run not only Java bytecode but also JavaScript, Python, Ruby, R, and LLVM-based languages (C/C++, Rust).
# GraalVM Native Image — AOT compilation
native-image -jar myapp.jar -o myapp
# Result: native binary with ~20ms startup time
# JVM: ~2s startup → GraalVM Native: ~20ms startup
GraalVM Native Image characteristics:
- AOT (Ahead-of-Time) compilation — no runtime JIT
- Fast startup — ideal for serverless and CLI tools
- Low memory — no JVM runtime overhead
- Limitations — restricted reflection, dynamic class loading
9. LLVM: The Modular Compiler Framework
LLVM is the standard infrastructure for modern compilers. Clang (C/C++), rustc (Rust), and swiftc (Swift) all use LLVM as their backend.
LLVM Architecture
Frontend Middle-end Backend
(per language) (shared) (per target)
┌→ x86_64
Clang ─┐ ┌→ Opt. Passes ────┤→ ARM
├→ LLVM IR ┤ └→ WASM
rustc ─┤ └→ Analysis Passes
│
swiftc ┘
LLVM IR Example
; Function: add(a, b) = a + b
define i32 @add(i32 %a, i32 %b) {
entry:
%result = add i32 %a, %b
ret i32 %result
}
; Function: factorial(n)
define i64 @factorial(i64 %n) {
entry:
%cmp = icmp sle i64 %n, 1
br i1 %cmp, label %base, label %recurse
base:
ret i64 1
recurse:
%n_minus_1 = sub i64 %n, 1
%sub_result = call i64 @factorial(i64 %n_minus_1)
%result = mul i64 %n, %sub_result
ret i64 %result
}
Key Optimization Passes
# Apply LLVM optimization passes
opt -O2 input.ll -o optimized.ll
# Key optimization passes:
# -mem2reg : Promote memory access to registers
# -inline : Function inlining
# -gvn : Global Value Numbering (remove redundant computation)
# -licm : Loop Invariant Code Motion
# -loop-unroll : Loop unrolling
# -sccp : Sparse Conditional Constant Propagation
# -dce : Dead Code Elimination
Constant Folding Example
// Before optimization
int x = 3 + 4;
int y = x * 2;
// After constant folding
int x = 7;
int y = 14;
Why LLVM matters:
- Modularity — frontends and backends can be developed independently
- Shared optimizations — every language benefits from the same optimization passes
- Easy new language development — just generate LLVM IR, get all platforms for free
10. Garbage Collection Algorithms
Mark-and-Sweep
The most fundamental GC algorithm.
1. Mark phase: Mark all reachable objects from roots
2. Sweep phase: Free memory of unmarked objects
Roots:
- Global variables
- Local variables on the stack
- Registers
[A] → [B] → [C]
↓
[D]
[E] → [F] (Unreachable from roots → collected)
Generational GC
Based on the Generational Hypothesis: most objects die young.
Young Generation (Minor GC — frequent, fast)
├── Eden Space (new object allocation)
├── Survivor 0
└── Survivor 1
Old Generation (Major GC — infrequent, slow)
└── Long-lived objects
GC process:
1. New objects → allocated in Eden
2. When Eden fills up → Minor GC
3. Survivors → moved to Survivor space
4. After N survivals → promoted to Old Generation
5. When Old Generation fills → Major GC
Modern GC Comparison
| GC | Used In | Features | Pause Target |
|---|---|---|---|
| G1 GC | JVM (default) | Region-based, predictable | Under 200ms |
| ZGC | JVM (latest) | Concurrent, pointer coloring | Under 1ms |
| Shenandoah | JVM (Red Hat) | Concurrent, Brooks pointers | Under 10ms |
| Orinoco | V8 (JS) | Generational, incremental, concurrent marking | Minimized |
| Ref Counting | CPython, Swift | Immediate dealloc, circular ref risk | None (distributed) |
| Ownership | Rust | No GC, compile-time memory management | None |
ZGC Key Idea
Traditional GC: Stop-the-World → Mark → Compact → Resume
(long pause)
ZGC: Application and GC run concurrently
(pointer coloring + load barriers for consistency)
Pointer Coloring:
Upper bits of 64-bit pointers used for GC metadata
[metadata bits][actual address bits]
- Marked0 bit: first marking cycle
- Marked1 bit: second marking cycle
- Remapped bit: object has been relocated
- Finalizable bit: finalizer pending
11. Build Tools Are Compilers
The tools frontend developers use every day leverage compiler technology.
Babel: JavaScript Transpiler
// Input: ES2022+
const greet = (name) => `Hello, ${name}!`;
const items = [1, 2, 3];
const doubled = items.map(x => x * 2);
// After Babel: ES5
"use strict";
var greet = function greet(name) {
return "Hello, " + name + "!";
};
var items = [1, 2, 3];
var doubled = items.map(function (x) {
return x * 2;
});
TypeScript Compiler (tsc)
TypeScript Source
↓
Parser → AST
↓
Binder → Symbol Table
↓
Type Checker → Type errors reported
↓
Emitter → JavaScript + .d.ts + .map
Next-Generation Bundler Comparison
| Tool | Language | Speed | Features |
|---|---|---|---|
| Webpack | JS | Baseline | Mature ecosystem, rich plugins |
| esbuild | Go | 10-100x faster | Parallel processing, fewer plugins |
| SWC | Rust | 20-70x faster | Babel-compatible, Next.js default |
| Turbopack | Rust | Incremental build optimized | Vercel, large projects |
| Vite | JS (esbuild/Rollup) | Very fast in dev | ESM-based dev server |
| Rspack | Rust | Webpack-compatible, fast | Webpack API compatible |
# esbuild benchmark example
# Bundling 10,000 modules:
# Webpack: 40 seconds
# esbuild: 0.4 seconds
Why esbuild and SWC are fast:
- Systems languages (Go/Rust) — no GC overhead, native speed
- Parallelism — aggressive use of multiple cores
- Minimal AST transforms — no unnecessary intermediate steps
- Zero-copy architecture — minimal memory copying
12. Interview Questions
Q1. What is the difference between a compiler and an interpreter?
A compiler translates the entire source code to machine code at once, while an interpreter executes code line by line. Most modern languages use a hybrid approach (bytecode compilation + JIT).
Q2. What is an AST and where is it used?
An AST (Abstract Syntax Tree) is a data structure that represents source code structure as a tree. It is used in compilers, linters (ESLint), formatters (Prettier), refactoring tools, and IDE autocompletion.
Q3. What are the pros and cons of JIT compilation?
Pros: aggressive optimization through runtime profiling, potentially better performance than AOT. Cons: warm-up time required, increased memory usage, compiler complexity.
Q4. What are V8 Hidden Classes?
Internal structure information V8 assigns to dynamic JavaScript objects. Objects with the same Shape enjoy faster property access.
Q5. What is Python's GIL?
The Global Interpreter Lock is a mutex in CPython that allows only one thread to execute bytecode at a time. This eliminates the benefits of multithreading for CPU-bound tasks.
Q6. How does Generational GC work?
Based on the hypothesis that most objects die young. It collects the Young Generation frequently for efficiency and the Old Generation infrequently.
Q7. Why is LLVM important in modern compilers?
Its modular architecture separates frontends (per language) from backends (per platform). New languages just need to generate IR to get all platforms and optimizations for free.
Q8. Why is esbuild faster than Webpack?
Written in Go for native speed and parallelism, no GC overhead, and performs minimal AST transformations.
Q9. What are the pros and cons of GraalVM Native Image?
Pros: fast startup (~20ms), low memory. Cons: longer build times, restricted reflection and dynamic features, peak performance may be lower than JIT.
Q10. What is ReDoS?
Regular Expression Denial of Service — when an inefficient regex causes exponential backtracking, consuming excessive CPU. Regex engines are themselves a form of interpreter.
13. Quiz
Q1. What does a lexer output?
A token stream. The lexer splits the source code string into tokens — meaningful units such as keywords, identifiers, literals, and operators.
Q2. When does TurboFan-optimized code get invalidated in V8?
Deoptimization occurs. When type assumptions TurboFan relied on break at runtime (e.g., a variable that was always an integer receives a string), V8 discards the optimized code and falls back to Ignition bytecode.
Q3. What is the difference between C1 and C2 compilers in the JVM?
C1 (Client Compiler) compiles quickly with basic optimizations. C2 (Server Compiler) takes longer but applies aggressive optimizations like inlining, loop unrolling, and vectorization. In Tiered Compilation, C1 runs first, and C2 is applied later to hot methods.
Q4. Why does LLVM IR use SSA form?
SSA (Static Single Assignment) is a form where each variable is assigned exactly once. This simplifies data flow analysis and enables optimizations like constant propagation and dead code elimination.
Q5. What is ZGC's pointer coloring?
A technique that uses the upper bits of 64-bit pointers for GC metadata (marking state, relocation status, etc.). This allows the GC to run concurrently with the application while maintaining reference consistency, achieving sub-millisecond pause times.
14. References
- Crafting Interpreters (Robert Nystrom) — The bible of interpreter implementation
- Compilers: Principles, Techniques, and Tools (Dragon Book) — The classic compiler theory text
- Engineering a Compiler (Cooper, Torczon) — Modern compiler engineering
- V8 Blog — https://v8.dev/blog
- Inside the V8 Engine (Marja Holtta) — Detailed V8 pipeline
- CPython Internals (Anthony Shaw) — CPython source code walkthrough
- The Architecture of Open Source Applications: LLVM — LLVM architecture
- LLVM Language Reference Manual — https://llvm.org/docs/LangRef.html
- Understanding the JVM — JVM internals
- ZGC: Scalable Low-Latency Garbage Collector — Oracle docs
- GraalVM Documentation — https://www.graalvm.org/docs/
- esbuild Architecture — https://esbuild.github.io/faq/
- SWC: Speedy Web Compiler — https://swc.rs/
- Modern Parser Generator (Laurence Tratt) — Parser generator comparison
- Tiered Compilation in JVM — https://docs.oracle.com/en/java/
- PyPy Documentation — https://doc.pypy.org/