What a Compiler Actually Does: From Source Code to Running Program

Dmitri Voronov

Dmitri Voronov

July 7, 2026

What a Compiler Actually Does: From Source Code to Running Program

When you write code in Python, Java, Rust, or C++, what you’re writing is text—human-readable instructions in a structured language that makes sense to people. What runs on your CPU is something completely different: a sequence of binary instructions—numbers that encode operations like “add these two values,” “store this in memory,” “jump to this address if the condition is true.” The distance between what a programmer writes and what a processor executes is enormous, and the software that bridges it—the compiler—is one of the most sophisticated tools in computing.

Most programmers use compilers constantly without knowing much about what happens inside them. Understanding the pipeline—from source text to running binary—illuminates a lot about why certain programs run fast, why certain bugs are hard to detect, and why language design choices have performance consequences that aren’t obvious at the source level.

What a Compiler Is and What It Isn’t

A compiler is a program that translates source code in one language into code in another language—typically from a high-level programming language to a lower-level representation, ultimately leading to machine code. The key characteristic of compilation is that the translation happens before execution: you compile once, then run the resulting program without the compiler being involved.

This is distinct from an interpreter, which reads and executes source code line by line at runtime without producing a separate translated artifact. Python (in its standard CPython implementation) is interpreted. C and Rust are compiled. Java is compiled to bytecode—an intermediate representation—which is then interpreted or just-in-time compiled by the Java Virtual Machine (JVM).

Many modern language implementations blur the line. JavaScript is technically interpreted but modern JS engines (V8, SpiderMonkey) use sophisticated just-in-time (JIT) compilation to compile hot code paths to native machine code during execution. Python has PyPy, which also uses JIT. The vocabulary of “compiled” and “interpreted” describes the dominant paradigm for a language but not a hard technical boundary.

Phase 1: Lexical Analysis (Scanning)

The first thing a compiler does is read the source text and break it into tokens—the fundamental units of the language’s syntax. A token might be a keyword (if, while, return), an identifier (a variable or function name), a literal (a number, string), an operator (+, ==, ->), or punctuation ({, ;, ().

The lexer (scanner) reads the raw character stream and groups characters into tokens using rules defined by the language’s lexical grammar. Whitespace and comments are discarded. The output is a stream of tokens, each carrying its type and content.

Consider this fragment of C code:

int x = a + 3;

The lexer produces tokens: int (keyword), x (identifier), = (assignment operator), a (identifier), + (plus operator), 3 (integer literal), ; (semicolon).

Lexical analysis is typically implemented using finite automata—state machines that recognize token patterns. Most compilers generate their lexers automatically from grammar specifications using tools like lex/flex.

Phase 2: Parsing (Syntactic Analysis)

The token stream doesn’t inherently convey structure. Parsing imposes grammatical structure—determining how tokens combine according to the language’s grammar to form statements, expressions, function definitions, and programs. The output of parsing is typically an Abstract Syntax Tree (AST)—a tree data structure that represents the grammatical structure of the source code.

The AST for int x = a + 3; might look like: a variable declaration node, with type “int”, name “x”, and initializer being an addition expression with left operand “a” (identifier) and right operand “3” (integer literal).

The grammar that the parser uses is typically a context-free grammar, specified formally (often in Backus-Naur Form). Parser generators like yacc/bison accept a grammar specification and generate a parser automatically. Hand-written parsers are also common for performance or control reasons—the Clang C++ compiler uses a hand-written recursive-descent parser.

Abstract syntax tree data structure visualization for programming language parsing

Parse errors occur when the token stream doesn’t conform to the grammar—a missing bracket, a statement outside a function, syntax that the language doesn’t allow. Good compilers not only detect these errors but provide useful error messages and attempt to recover to detect further errors without stopping at the first problem.

Phase 3: Semantic Analysis

After parsing produces a syntactically valid AST, semantic analysis checks that the program makes sense according to the language’s type system and scoping rules. A program can be syntactically valid but semantically nonsensical: adding a string to an integer (in a statically typed language), referencing a variable before it’s declared, calling a function with the wrong number of arguments.

Semantic analysis involves:

Type checking: Verifying that operations are applied to compatible types. In statically typed languages like C, Rust, and Java, this catches type errors at compile time. Type inference (in languages like Rust and Haskell) means the programmer doesn’t always need to write type annotations; the compiler deduces them.

Scope resolution: Determining which variable declaration each identifier reference refers to—handling shadowing, nested scopes, and forward declarations.

Control flow analysis: Checking that functions with non-void return types always return a value, detecting unreachable code, and in some languages (like Rust), verifying that variables are initialized before use.

Borrow checking (Rust): Rust’s ownership and borrowing rules are enforced by a specialized component of the compiler during semantic analysis—this is what makes Rust’s memory safety guarantees possible at compile time without garbage collection.

Semantic analysis annotates the AST with type information and produces error messages for any semantic violations found. In a well-designed compiler, this phase catches the large majority of programmer errors that would otherwise produce buggy runtime behavior.

Phase 4: Intermediate Representation and Optimization

After semantic analysis, compilers typically translate the AST to an intermediate representation (IR)—a lower-level, language-neutral format that’s easier to analyze and optimize than the AST but more abstract than machine code. LLVM IR is a famous example: a typed, SSA-form (static single assignment) instruction set that represents operations in a way that makes many optimizations straightforward to apply.

SSA form means each variable is defined exactly once. When a variable would be reassigned (like a loop counter), new “versions” of the variable are created. This property makes many optimizations much easier—constant propagation, dead code elimination, and detecting when a value is available in a register without recomputation become cleaner problems.

Optimization is where compilers do a lot of their most intellectually interesting work. Common optimizations include:

Constant folding: Evaluating constant expressions at compile time. x = 2 + 3 becomes x = 5.

Dead code elimination: Removing code that can never be reached or whose result is never used.

Inlining: Replacing a function call with the function’s body. Eliminates call overhead and enables further optimizations across the call boundary.

Loop optimizations: Including loop unrolling (reducing loop overhead by duplicating the loop body), loop-invariant code motion (moving calculations that don’t change across iterations outside the loop), and vectorization (converting scalar loops to SIMD operations that process multiple data elements per instruction).

Register allocation: Deciding which variables should be kept in CPU registers (fast) versus memory (slower). Since CPUs have a limited number of registers, the compiler must determine the optimal assignment—a problem related to graph coloring, an NP-complete problem in general that compilers solve heuristically.

CPU processor chip executing low-level machine code instructions at high speed

Phase 5: Code Generation

The final phase translates the optimized IR to machine code—the binary instructions specific to the target CPU architecture (x86-64, ARM64, RISC-V, etc.). Code generation involves:

Instruction selection: Choosing which machine instructions to use for each IR operation. A single abstract operation might map to several possible instruction sequences on a given architecture; the code generator picks efficient ones.

Instruction scheduling: Ordering instructions to minimize pipeline stalls. Modern CPUs execute multiple instructions simultaneously (out-of-order execution, superscalar execution) but have dependencies between instructions—if instruction B needs the result of instruction A, B must wait for A to complete. The compiler can reorder independent instructions to keep the CPU’s execution units busy.

Final register allocation: Mapping virtual registers from the IR to actual hardware registers, inserting “spill” code that saves register values to memory and reloads them when the register is needed for something else.

The output is an object file—machine code in a format that still needs linking.

The Linker: Combining the Pieces

A large program is compiled as multiple source files, each producing a separate object file. The linker combines these object files (and library files) into a final executable, resolving references between them. When function A in one file calls function B in another, the object file for A contains an unresolved reference to B. The linker finds B’s definition, calculates B’s address in the final executable, and patches A’s call instruction to use that address.

Static linking includes all library code in the executable. Dynamic linking leaves certain library references unresolved, to be resolved at runtime when the dynamic library (.dll on Windows, .so on Linux) is loaded. Dynamic linking allows multiple programs to share a single copy of a library in memory and allows libraries to be updated without recompiling the programs that use them.

Interpreted Languages and JIT Compilation

For interpreted languages like Python, there’s no separate compilation step before execution. The interpreter reads source code (or a pre-parsed bytecode representation) and executes it directly. This is simpler and enables fast development cycles—edit a file, run it immediately—but is typically slower than compiled code because the interpretation overhead happens at runtime.

Just-in-time (JIT) compilation combines the flexibility of interpretation with the speed of compilation. The JIT compiler monitors which code paths are “hot”—executed frequently—and compiles those to native machine code while the program runs. Subsequent executions of the hot code path run the compiled version at near-native speed. The JVM, V8, and modern CLR implementations all use JIT compilation extensively, which is why Java and JavaScript performance can approach C++ for computationally intensive workloads despite being “interpreted” languages in the traditional sense.

Why This Matters for Programmers

Understanding the compiler pipeline illuminates several practical things:

Compiler optimizations explain why writing “obvious” code is often fine: the optimizer will often produce the same machine code whether you write the loop in a naive form or a pre-optimized form. It also explains why some patterns are hard to optimize and require programmer intervention—function pointers, complex aliasing, and dynamic dispatch can defeat optimizations that work on simpler code.

Compile-time errors exist because semantic analysis can catch mistakes before they become runtime bugs. Rust’s borrow checker prevents entire categories of memory safety bugs that C and C++ leave to runtime. Type errors in statically typed languages are cheaper to find at compile time than at runtime in production.

The difference between compiled and interpreted execution speeds comes from all the work a compiler does offline: optimizing IR, allocating registers, scheduling instructions. An interpreter does none of this (or does it at runtime with JIT), so the first execution of interpreted code is typically slower.

Compilers are one of the most successful examples of complex software that works reliably at scale. Modern compilers like GCC, Clang/LLVM, and rustc are millions of lines of code that transform human-readable text into programs that run on billions of devices. The discipline of compiler construction—from the formal language theory of parsing to the practical engineering of register allocators—has been one of the richest areas of computer science for the past seven decades, and remains foundational to everything that runs on modern hardware.

More articles for you