Skip to content
Published on

Compilers and Interpreters: How Code Actually Runs — Everything Developers Need to Know

Authors

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
StageInputOutputKey Role
LexingSource stringToken streamSplit text into meaningful units
ParsingToken streamASTBuild a tree of grammatical structure
Semantic AnalysisASTType-checked ASTType checking, scope resolution
IR GenerationASTIntermediate RepresentationPlatform-independent intermediate code
OptimizationIROptimized IRConstant folding, inlining, etc.
Code GenerationOptimized IRMachine code / BytecodeGenerate 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

FeaturePure CompilerPure InterpreterJIT Compiler
ExamplesC, C++, Rust, GoShell script, early BASICJavaScript (V8), Java (HotSpot)
Pre-executionFull compilation requiredNoneBytecode compilation
Execution speedVery fastSlowFast (after warm-up)
Startup timeSlow (compile time)FastMedium
Memory usageLowMediumHigh (includes compiler)
DebuggingHarderEasierMedium
Optimization levelStatic analysisNoneRuntime 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
  ParserAST
  Ignition (Bytecode Interpreter)
     (Collects profiling data)
  TurboFan (Optimizing JIT Compiler)
  Optimized Machine Code
     (Deoptimizationif 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

  1. Always initialize object properties in the same order
  2. Keep argument types consistent for functions
  3. Do not mix types in arrays (no strings in number arrays)
  4. Use obj.prop = undefined instead of delete obj.prop
  5. 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
FeatureC1 (Client)C2 (Server)
Compile speedFastSlow
Optimization levelBasicAggressive
InliningLimitedAggressive
Loop optimizationBasicUnrolling, vectorization
When usedEarlyHot 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

GCUsed InFeaturesPause Target
G1 GCJVM (default)Region-based, predictableUnder 200ms
ZGCJVM (latest)Concurrent, pointer coloringUnder 1ms
ShenandoahJVM (Red Hat)Concurrent, Brooks pointersUnder 10ms
OrinocoV8 (JS)Generational, incremental, concurrent markingMinimized
Ref CountingCPython, SwiftImmediate dealloc, circular ref riskNone (distributed)
OwnershipRustNo GC, compile-time memory managementNone

ZGC Key Idea

Traditional GC:  Stop-the-WorldMarkCompactResume
                 (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
  ParserAST
  BinderSymbol Table
  Type CheckerType errors reported
  EmitterJavaScript + .d.ts + .map

Next-Generation Bundler Comparison

ToolLanguageSpeedFeatures
WebpackJSBaselineMature ecosystem, rich plugins
esbuildGo10-100x fasterParallel processing, fewer plugins
SWCRust20-70x fasterBabel-compatible, Next.js default
TurbopackRustIncremental build optimizedVercel, large projects
ViteJS (esbuild/Rollup)Very fast in devESM-based dev server
RspackRustWebpack-compatible, fastWebpack API compatible
# esbuild benchmark example
# Bundling 10,000 modules:
# Webpack:   40 seconds
# esbuild:    0.4 seconds

Why esbuild and SWC are fast:

  1. Systems languages (Go/Rust) — no GC overhead, native speed
  2. Parallelism — aggressive use of multiple cores
  3. Minimal AST transforms — no unnecessary intermediate steps
  4. 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

  1. Crafting Interpreters (Robert Nystrom) — The bible of interpreter implementation
  2. Compilers: Principles, Techniques, and Tools (Dragon Book) — The classic compiler theory text
  3. Engineering a Compiler (Cooper, Torczon) — Modern compiler engineering
  4. V8 Bloghttps://v8.dev/blog
  5. Inside the V8 Engine (Marja Holtta) — Detailed V8 pipeline
  6. CPython Internals (Anthony Shaw) — CPython source code walkthrough
  7. The Architecture of Open Source Applications: LLVM — LLVM architecture
  8. LLVM Language Reference Manualhttps://llvm.org/docs/LangRef.html
  9. Understanding the JVM — JVM internals
  10. ZGC: Scalable Low-Latency Garbage Collector — Oracle docs
  11. GraalVM Documentationhttps://www.graalvm.org/docs/
  12. esbuild Architecturehttps://esbuild.github.io/faq/
  13. SWC: Speedy Web Compilerhttps://swc.rs/
  14. Modern Parser Generator (Laurence Tratt) — Parser generator comparison
  15. Tiered Compilation in JVMhttps://docs.oracle.com/en/java/
  16. PyPy Documentationhttps://doc.pypy.org/