College · 30 classes

Computer Science - Compilers

Each class is a short animated explainer with narration and illustrations, plus quick checks and a mastery quiz. Your progress saves automatically as you complete classes.

▶ Watch class 1 free — no sign-up
Progress map · 30 classes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Not startedStartedCompletedMastered
01
What Is a Compiler, and Why Should We Care?
Not started
Start →
02
The Anatomy of a Compiler: A High-Level Tour
Not started
Start →
03
Formal Languages and Automata: The Chomsky Hierarchy
Not started
Start →
04
Our First Project: Compiling a Calculator
Not started
Start →
05
Regular Expressions: The Universal Language of Patterns
Not started
Start →
06
From Regex to NFA: Thompson's Construction
Not started
Start →
07
From NFA to DFA: The Subset Construction Algorithm
Not started
Start →
08
Case Study: Building a Real Lexer with Flex
Not started
Start →
09
Context-Free Grammars: Describing Language Syntax
Not started
Start →
10
Top-Down Parsing: The Challenge of Recursive Descent
Not started
Start →
11
Bottom-Up Parsing: The Power of Shift-Reduce
Not started
Start →
12
Taming Ambiguity: SLR, LALR(1), and the 'Dragon Book' Parsers
Not started
Start →
13
Case Study: Building a Real Parser with Bison
Not started
Start →
14
Beyond Syntax: The Role of the Semantic Analyzer
Not started
Start →
15
Symbol Tables: How a Compiler Remembers Names
Not started
Start →
16
Type Checking: Enforcing the Rules of the Language
Not started
Start →
17
Attribute Grammars: Decorating the Parse Tree
Not started
Start →
18
The Abstract Syntax Tree: The Compiler's Central Data Structure
Not started
Start →
19
Three-Address Code: A Universal Language for Machines
Not started
Start →
20
Control Flow Graphs: Mapping the Program's Journey
Not started
Start →
21
Static Single Assignment (SSA): The Modern IR of Choice
Not started
Start →
22
What Is 'Better' Code?: An Introduction to Optimization
Not started
Start →
23
Dataflow Analysis: Reaching Definitions and Liveness
Not started
Start →
24
Classic Optimizations: From Peepholes to Program-wide Changes
Not started
Start →
25
Loop Optimizations: The Engine of High Performance
Not started
Start →
26
The Alias Analysis Problem: Who Is Pointing to What?
Not started
Start →
27
Instruction Selection: Speaking the Language of the CPU
Not started
Start →
28
Register Allocation: The Art of Resource Management
Not started
Start →
29
Runtime Environments and Garbage Collection
Not started
Start →
30
The Modern Compiler: JITs, DSLs, and the Future
Not started
Start →
See inside a class

Here’s all of Class 1, in full.

Every class is 13 cards · narrated film + illustration · 2 quick checks · an interactive · a 4-question mastery quiz. Nothing hidden — this is the complete text of What Is a Compiler, and Why Should We Care?.

▸ Read the full class — What Is a Compiler, and Why Should We Care?

Every piece of software you use, from the operating system on your laptop to the app on your phone, was born from an act of translation. A human had an idea, expressed it in a language like C++ or Swift, and another program transformed that idea into the raw binary instructions a processor can execute. That translator is a compiler. It is arguably the most influential type of program ever conceived, because it's the tool that builds all other tools. Without it, modern computing would be impossible. Our entire digital world is built on a foundation of automated translation, a process so successful we almost never think about it. Today, we're going to start thinking about it.

1. The Chasm Between Human and Machine

Raw machine code is a language of numbers, fundamentally alien to human logic.

The core problem is a vast semantic gap. On one side, we have human thought, which thrives on abstraction, symbolism, and complex logical structures. We write `if x > 10, then...`. On the other side, we have a central processing unit, which understands a very rigid, primitive language: the instruction set architecture. It understands operations like 'load this value from memory address 8048 into register EAX' or 'add the value in register EBX to EAX'. Directly writing machine code is feasible for trivial programs. But for a system with millions of lines of logic, it is completely untenable. The cognitive load is too high, the process is error-prone, and the result is tied to one specific type of hardware. If a new processor comes out, the entire software base must be rewritten from scratch. This chasm between human intent and machine execution is the fundamental problem that compilers exist to solve. Without an automated, reliable bridge, software cannot scale in complexity, portability, or correctness.

  • This chasm between human intent
  • It understands operations like 'load this value from memory address 8048
  • Directly writing in this machine language is feasible for trivial programs
  • The cognitive load is too high, the process is astronomically error-prone
  • The entire software base must be rewritten from scratch

2. A Compiler Is a Language Translator

Formally, a compiler is a program that translates source code from a high-level language to a lower-level language.

Let's be precise. A compiler is a program, C, that performs a translation. It takes as input a program P_S, written in a source language S, and produces an equivalent program P_T, written in a target language T. We can write this as a function: C(P_S) = P_T. The word 'equivalent' here is critical. The compiler must preserve the *semantics*—the meaning—of the original program. If the source program calculated mortgage payments, the target program must also calculate mortgage payments, with identical results. This semantic preservation is the compiler's primary contract. The source language, S, is typically a high-level language like Java, C++, or Rust. The target language, T, is often the instruction set of a specific processor, like x86-64 or ARMv8. However, this isn't always the case. A program that translates TypeScript to JavaScript is also a compiler, often called a 'transpiler'. A Java compiler translates Java source into JVM bytecode, an intermediate language. The core concept is a transformation from one formal language to another, while holding the meaning constant.

  • The target language is often the instruction set of a specific
  • A Java compiler translates Java source into JVM bytecode
  • A program that translates TypeScript to JavaScript is also a compiler
  • A compiler is a program that performs a translation
  • The compiler must preserve the semantics—the meaning—of the original program

3. From Hopper's A-0 to Backus's FORTRAN

The first compilers weren't built for convenience; they were built to make programming possible at all.

The idea of automated translation emerged from necessity in the early 1950s. Programming early machines like the UNIVAC I was a painstaking process of writing numerical codes. Grace Hopper, a key figure in this era, developed a system called A-0 in 1952. While not a compiler in the modern sense—it was more of a system for linking together pre-written subroutines identified by numeric codes—Hopper herself 'coined' the term compiler. She envisioned 'compiling' a program from a library of existing pieces. The true breakthrough came between 1954 and 1957 with John Backus's team at IBM. They were developing FORTRAN, which stands for Formula Translation. Their goal was audacious and widely seen as impossible: to create a high-level language for scientific computing that a compiler could translate into machine code as efficient as that written by a skilled human assembly programmer. The prevailing belief was that any automated translation would be unacceptably slow. The first FORTRAN compiler was a massive, complex piece of software, but it worked. It proved that high-level programming was not just possible, but practical, legitimizing the entire field of compiler construction.

  • Programming early machines like the UNIVAC I was a painstaking process
  • The first FORTRAN compiler was a massive, complex piece of software
  • Their goal was audacious and widely seen as impossible: to create
  • It proved that high-level programming was not just possible, but practical
  • The true breakthrough came between 1954

4. The Classic Three-Stage Architecture

How do you translate an idea into instructions? By systematically breaking it down.

To manage the immense complexity of translation, compilers are almost universally designed with a three-stage architecture. This separation of concerns is one of the most elegant ideas in software engineering. First is the **front end**. Its job is to understand the source language. It performs lexical analysis to break the text into tokens, parsing to check the grammar and build an Abstract Syntax Tree (or AST), and semantic analysis to check for type errors and other meaning-related issues. The output of the front end is a language-independent data structure called an Intermediate Representation, or IR. Second is the **middle end**, or optimizer. This stage takes the IR and transforms it to make it better, usually meaning faster or smaller. It performs a host of analyses and transformations—like dead code elimination, constant folding, and loop optimizations—all on the IR, without any knowledge of the original source language or the final target machine. Finally, we have the **back end**, or code generator. It takes the optimized IR and maps it onto the target architecture. This involves instruction selection (choosing the right machine instructions), register allocation (managing the CPU's limited number of registers), and emitting the final machine code or assembly. This modular design means that to support a new language, we only need a new front end. To support a new processor, we only need a new back end.

  • It performs lexical analysis to break the text into tokens
  • This involves instruction selection, register allocation
  • This stage takes the IR
  • It performs a host of analyses
  • The output of the front end is a language-independent data structure

5. Representing the Compilation Process

To reason about compilers, especially in complex scenarios like bootstrapping where a compiler compiles itself, we use a notation called T-diagrams, developed by McKeeman, Horning, and Wortman. A T-diagram visually represents a compiler. The 'T' shape has three parts. The top-left arm represents the source language, S. The top-right arm represents the target language, T. The bottom, the stem of the T, represents the implementation language, L—the language the compiler itself is written in. So, a diagram with C on the left, x86 on the right, and x86 on the bottom represents a C-to-x86 compiler that is itself an x86 executable. We can combine these diagrams. For example, to show how we run a compiler, we can place its T-diagram on top of a diagram representing the machine it runs on. If we have a C-to-x86 compiler written in C, its T-diagram has C, x86, and C. To produce an executable version, we need an existing compiler to translate it. This process of chaining diagrams allows us to formally describe how compilers are built and ported to new systems. It's a simple but powerful notation for reasoning about our tools.

  • A diagram with C on the left, x86 on the right
  • The bottom, the stem of the T, represents the implementation language
  • Especially in complex scenarios like bootstrapping where a compiler compiles itself
  • To show how we run a compiler
  • We need an existing compiler to translate

6. Correctness, Performance, and Abstraction

What separates a great compiler from a merely functional one?

We can judge a compiler by several key properties. These are the goals that compiler developers strive for. First, and most importantly, is **correctness**. The compiler must preserve the meaning of the program. Any translation that changes what the program does is a failure. This is the bedrock on which everything else is built. Second is the **quality of the generated code**. This usually means performance. The target program should be fast and efficient. As we saw with FORTRAN, a primary motivation for compilers is to generate code that rivals or exceeds the quality of human-written assembly. Third is **abstraction and portability**. By separating the high-level source from the low-level machine details, compilers allow us to write code once and run it on many different types of hardware. The compiler handles the messy details of each specific architecture. Fourth is the quality of its **diagnostics**. When the programmer makes a mistake and provides an invalid program, the compiler must provide fast, accurate, and helpful error messages. A cryptic error message can be as much of a hindrance as a bug in the code generator. A great compiler is a partner in the development process.

  • The compiler must preserve the meaning of the program
  • Is the quality of the generated code
  • Compilers allow us to write code once
  • Is the quality of its diagnostics

7. Compiling a Single Line of C

Let's trace a simple C assignment statement through the front end: `y = a * x + b;`. First, the lexical analyzer, or lexer, scans this text and produces a stream of tokens. It sees 'y' and emits an IDENTIFIER token with the value 'y'. It sees '=' and emits an EQUALS token. This continues for `a`, `*`, `x`, `+`, `b`, and the semicolon. The result is a sequence of categorized symbols, stripped of whitespace. Next, the parser consumes this token stream. It applies grammar rules to ensure the sequence is valid—for example, that an expression appears on the right side of the equals sign. It builds an Abstract Syntax Tree, or AST, which captures the hierarchical structure. The root of this tree would be the assignment. Its left child would be the identifier `y`, and its right child would be a subtree for the addition. The addition node, in turn, would have children for the multiplication and the identifier `b`. Then, semantic analysis walks this tree. It checks a symbol table to ensure `y`, `a`, `x`, and `b` have been declared and that their types are compatible for multiplication and addition. Finally, this validated tree is translated into a linear Intermediate Representation, like three-address code. This might look like: `t1 = a * x;`, `t2 = t1 + b;`, `y = t2;`. This IR is now ready for the optimizer and back end.

  • It builds an Abstract Syntax Tree, or AST
  • The lexical analyzer, or lexer
  • This validated tree is translated into a linear Intermediate Representation
  • Its left child would be the identifier
  • This IR is now ready for the optimizer and back end

8. The Compilation 'Iron Triangle'

You can't have everything. A fast compiler, highly optimized code, and broad target support are often in tension.

Compiler design is a game of managing tradeoffs. There is no single 'best' compiler, only compilers that are best for a particular purpose. The most fundamental tradeoff is **compilation speed versus the quality of the generated code**. Running sophisticated optimization algorithms takes time. A 'debug' build, used during development, prioritizes compilation speed. It will therefore disable most optimizations. A 'release' build, for shipping to users, prioritizes runtime performance. It will enable aggressive optimizations, even if the compilation process takes several minutes or longer. Another tension exists between **generality and specificity**. A general-purpose compiler infrastructure like LLVM is incredibly powerful and supports dozens of languages and targets, but it is a massive and complex piece of software. A domain-specific compiler, perhaps for a specialized hardware accelerator, might be much simpler to build and generate better code for its narrow domain, but it lacks flexibility. Finally, there's a tradeoff between **language expressiveness and static analyzability**. Languages with very dynamic features, pointers, or complex metaprogramming can be very difficult for a compiler to analyze and optimize safely. The compiler must often make conservative assumptions, leading to slower code. Simpler, more restrictive languages can often be optimized more effectively.

  • A general-purpose compiler infrastructure like LLVM is incredibly powerful
  • Languages with very dynamic features, unrestricted pointers
  • Compiler design is a game of managing tradeoffs
  • There is no single 'best' compiler
  • A 'debug' build, used during development

9. Compilers, Interpreters, and JITs

Compilation isn't the only way to execute a program. Let's place it in context.

It's useful to contrast compilers with two other common execution models: interpreters and just-in-time compilers. A pure **interpreter** reads the source code directly and executes it line by line. It doesn't produce a separate target file. Think of classic BASIC or Python's default implementation. The main advantage is platform independence and ease of development—there's no separate compile step. The significant disadvantage is performance; the overhead of parsing and analyzing each line every time it's executed is substantial. A traditional **compiler**, as we've discussed, performs this translation ahead-of-time (AOT). It produces a native executable that can be run directly by the operating system. This yields very high performance but requires a separate compilation step and produces platform-specific binaries. A **Just-In-Time (JIT) compiler** offers a hybrid approach. The program starts running under an interpreter. The runtime system monitors the code as it executes, identifying 'hot spots'—frequently executed loops or functions. These hot spots are then compiled to native machine code on the fly, in memory. Subsequent calls to that code will use the fast, compiled version. This is the model used by the Java Virtual Machine and modern JavaScript engines like V8. It combines the portability of an interpreter with performance approaching that of an AOT compiler, at the cost of runtime complexity and a 'warm-up' period.

  • Model used by the Java Virtual Machine
  • It's useful to contrast compilers with two other common
  • A pure interpreter reads the source code directly
  • The significant disadvantage is performance; the overhead of parsing
  • It produces a native executable that can be run

10. Where Intuition Can Go Wrong

The compiler is a powerful tool, but misunderstanding it leads to subtle bugs and performance issues.

As you become a more advanced programmer, it's critical to have an accurate mental model of what the compiler does. Several common pitfalls can lead you astray. The first is viewing the compiler as a simple, literal translator. Believing that your C code maps one-to-one to a sequence of machine instructions is false. Modern compilers are incredibly aggressive. They will reorder instructions, eliminate entire blocks of code they can prove are unused, and inline functions. Your source code is a *specification* of behavior, not a direct script for the CPU. This leads to the second pitfall: ignoring 'undefined behavior'. Certain operations in languages like C and C++ are not defined by the standard. A common example is signed integer overflow. Your intuition might be that it just wraps around. But because the behavior is undefined, the compiler is free to assume it never happens. This assumption allows for powerful optimizations, but if your code does trigger it, the program can behave in completely nonsensical ways. Finally, a common mistake is to engage in micro-optimizations based on flawed assumptions about what is 'fast'. You might think pointer arithmetic is faster than array indexing, or that one looping construct is better than another. In reality, the compiler is often much smarter about these things. Always profile your code before and after making performance changes. The compiler's optimizer is your partner, not your adversary.

  • They will reorder instructions, eliminate entire blocks of code
  • The first is viewing the compiler as a simple, literal translator
  • The compiler is often much smarter
  • Believing that your C code maps one-to-one to a sequence
  • Your source code is a specification of behavior

11. Essential Texts and Infrastructures

How do you go from theory to practice? By using the right tools and reading the right books.

To deepen your understanding of this material, you need to engage with the tools and literature of the field. The canonical textbook is *Compilers: Principles, Techniques, & Tools* by Aho, Lam, Sethi, and Ullman. It's universally known as the 'Dragon Book' because of its cover art. While some parts are dated, its theoretical foundations are timeless. In modern practice, two open-source projects dominate the landscape: GCC, the GNU Compiler Collection, and LLVM. LLVM, in particular, is designed as a modular set of libraries, not a monolithic program. Its well-defined Intermediate Representation, the LLVM IR, has become a de facto standard. Understanding the architecture of these systems is crucial for any serious work in the field. For building the front-end of a compiler, tools called parser generators like ANTLR or Bison are indispensable. They take a formal grammar for a language as input and automatically generate the parsing code for you. Finally, an invaluable tool for exploration is Matt Godbolt's Compiler Explorer. It's a web-based tool that lets you type in code and see the assembly output generated by a wide variety of compilers and settings, in real time. It makes the compiler's transformations tangible.

  • The canonical textbook is Compilers: Principles
  • Two open-source projects dominate the landscape: GCC
  • An invaluable tool for exploration is Matt Godbolt's Compiler Explorer
  • For building the front-end of a compiler
  • LLVM, in particular, is designed as a modular set

12. Observe Your Compiler in Action

Theory is essential, but seeing is believing. Let's make optimization concrete.

This week's exercise is designed to make the abstract concept of optimization concrete. Your task is to write a simple C or C++ function. It could be anything: a function to compute the nth Fibonacci number, a factorial, or a simple loop that sums the elements of an array. Then, using either `clang` or `gcc` on a command line, compile this file multiple times. Use the `-S` flag to produce human-readable assembly output, and try different optimization level flags: `-O0` for no optimization, `-O1`, `-O2`, and finally `-O3` for the most aggressive optimizations. You don't need to be an assembly expert to do this. Your goal is to observe the *differences*. Use a tool like `diff` to compare the output files. How many lines of code are there at `-O0` versus `-O3`? Can you spot where a loop might have been unrolled or where calculations have been done at compile time? Write a short, one-page report summarizing your observations for one function. This will give you a visceral sense that the compiler is not a passive translator, but an active agent in shaping your program's performance.

  • It could be anything: a function to compute the nth Fibonacci
  • This will give you a sense that the compiler is not
  • This week's exercise is designed to make the abstract concept
  • Then, using either clang or gcc on a command line
  • Use a tool like diff to compare the output files

13. The Compiler: Your Silent Partner

We've defined a compiler as a semantic-preserving translator that bridges the gap between human-readable source and machine-executable code. This tool is not merely a convenience; it's a foundational technology that enables abstraction, performance, and portability in all of modern software.

  • A compiler is a translator from a high-level source language to a low-level target language.
  • The canonical architecture separates compilation into a front end, a middle end, and a back end.
  • Compilers enable hardware independence, allowing high-level code to be portable.
  • A fundamental tradeoff exists between compilation speed and the performance of the generated code.
  • Modern tools like LLVM and Godbolt make compiler behavior transparent and explorable.

Mastery quiz

  1. Who coined the term 'compiler', and with what early-1950s system?
    • John Backus, with FORTRAN
    • Grace Hopper, with the A-0 system
    • McKeeman, with T-diagrams
    • Grace Hopper, with the UNIVAC I
  2. What does the modular three-stage design let you reuse when supporting a brand-new processor?
    • Only a new front end is needed; the rest is reused
    • Only a new back end is needed; the front and middle ends are reused
    • The entire compiler must be rewritten
    • Only the optimizer must be replaced
  3. How does a Just-In-Time (JIT) compiler differ from a traditional ahead-of-time compiler?
    • It never produces native machine code
    • It compiles frequently executed 'hot spots' to native code on the fly while the program runs
    • It translates the whole program to a platform-specific binary before running
    • It only works for statically typed languages
  4. Per the 'iron triangle', what is the most fundamental tradeoff in compiler design?
    • Source language versus target language
    • Compilation speed versus the quality of the generated code
    • Front end versus back end complexity
    • Interpreters versus transpilers
HomePracticeFeedBlogMe