How Compilers Work: From Source Code to Machine Code

15 min read · Updated: October 23, 2026

A compiler is software that translates a program written by people into the 0-and-1 instructions a CPU can run (machine code). It translates the whole program at once, before the program runs. Along the way, it also finds some mistakes and rewrites the code to get the same result with less work (optimization). A CPU can only run machine code, so this translation step is essential.

What you'll learn

  • Why a CPU cannot run a program exactly as people write it
  • The steps that turn one line of code into machine code (split into words → build a tree → check → optimize → make instructions → link)
  • How compilers, interpreters and JIT differ, and what Java, JavaScript and Python actually do
  • Which mistakes a compiler can catch, and which it cannot

Prerequisites: none. It helps to know that a CPU is "the part that runs simple instructions written in 0s and 1s, one after another."

In everyday life

When you install an app on a PC, most of what you download is a program already translated into machine code. The same game often comes as separate files for PCs and for phones. Programmers often say things like "the build passed" or "I got a compile error."

Behind all of this is a compiler. This article follows a compiler's work step by step, using one line of code as the example.

Why translation is needed

The only thing a CPU can run directly is instructions written as patterns of 0s and 1s. A program in this form is called "machine code." Each instruction is a tiny task, such as "load a number," "add" or "compare." "What Is a CPU?" explains this in more detail.

Machine code is hard for people to read and hard to write. So people write programs in a form closer to English words and math formulas. The set of rules for writing this way is called a "programming language." Examples include C, Java, JavaScript and Python.

The text of a program written by a person is called "source code." Source code is just a string of characters, and it means nothing to a CPU. So something has to translate it into machine code.

Software that translates a whole program into machine code before it runs is called a "compiler." The kinds of machine instructions, and how they are encoded, differ from one type of CPU to another. So a compiler first decides which CPU it is translating for. This is one reason PC and phone versions of an app come as different files.

From one line of code to machine code

As an example, we will use the line below, which converts hours h into seconds s. The "*" means multiply, and ";" marks the end of a statement.

s = 60 * 60 * h;

To keep things simple, we use a compiler for a C-like language and split its work into seven steps. Real compilers divide and name the steps a little differently.

How one line of code becomes an executable file in seven steps Source code s = 60 * 60 * h; ① Split into words s = 60 * 60 * h ; ② Check grammar, build a tree (60 × 60) × h, put into s ③ Check meaning Has h been set up? ④ Optimize 60 × 60 → 3600 ⑤ Make instructions Load h × 3600 Store in s ⑥ Turn into 0s and 1s Machine code (0s and 1s) ⑦ Link with other parts An executable file is ready
Steps ①–⑤ are the compiler's job, ⑥ is the assembler's, and ⑦ is the linker's. The step ⑤ instructions are written in plain words to show the idea. Left out: how real instructions are written (it depends on the CPU type), that some compilers do ⑤ and ⑥ in one program, and the in-between forms used along the way.

① Split into words (lexical analysis)

To a compiler, source code starts out as nothing but a string of characters. So first, it cuts the string into meaningful pieces. "60" is one number, "*" is one symbol, and so on. This step is called lexical analysis. The spaces are dropped, and the example line becomes eight words.

② Check the grammar and build a tree (parsing)

Next, the compiler checks whether the words follow the grammar of the language. For example, two symbols in a row, as in "60 * * h," are reported as a grammar error. This step is called parsing.

If the grammar is correct, the compiler arranges the calculation into a tree shape. Multiplication is done from left to right. So 60 × 60 comes first, and that result is then multiplied by h. The "=" at the top means to put the result of the right side into s. This tree is called a syntax tree. Its formal name is "abstract syntax tree" (AST).

Optimization replaces the "60 × 60" part of the syntax tree with 3600 Before After = s × × h 60 60 = s × 3600 h
Left: the syntax tree of "s = 60 * 60 * h;" before optimization. The values at the bottom of the tree are calculated first, and the "=" at the top finally puts the result into s. The part inside the dotted box has a known answer before the program runs, so it is replaced with 3600, as on the right (after optimization). Left out: other information a real tree records, such as the type of each value.

③ Check the meaning

Even when the grammar is correct, the code may not make sense. For example, h is a named container for a value (a variable). If h was never set up anywhere, the calculation cannot be done. The compiler checks that every name used in the program has been set up.

The kind of a value, such as a number or text, is called its "type." The compiler also checks for calculations where the types do not match. One example is multiplying a number by a string of characters like "abc."

④ Rewrite it to do less work (optimization)

The compiler rewrites the program to make it faster or smaller, without changing the result. This is called "optimization."

In the example, the answer to "60 × 60" is known to be 3600 before the program even runs. So the compiler replaces this part of the tree with 3600. This kind of optimization is called constant folding.

⑤ Turn it into CPU instructions (code generation)

The optimized result is then turned into a sequence of CPU instructions. For the example line, the instructions do roughly three things: load the value of h, multiply it by 3600, and write the result to where s is kept. The actual number and form of the instructions depend on the type of CPU.

The output of this step is often machine instructions written as short symbols, shortened from English words. This way of writing is called "assembly language."

⑥ Turn it into 0s and 1s (the assembler)

An assembler is software that replaces assembly language instructions with the patterns of 0s and 1s a CPU can read. Usually, a single compiler command runs steps ⑥ and ⑦ automatically as well.

⑦ Connect the parts (the linker)

Large programs are written in many separate files. Common tasks, such as showing text on the screen, use ready-made collections of code. Such a collection is called a "library."

A linker is software that connects already translated parts into one executable file. It fills in where each task is found, such as "this task is at this place in that part". A library can be built into the executable file, or loaded when the program runs. "Static vs Dynamic Linking" compares the two in a table.

Running the program is the OS's job

The finished executable is a file saved in storage. On PCs and phones, a program is placed in memory before it runs. When you tap an icon, the OS loads the file's contents into memory. It also prepares the libraries the program needs while running, then has the CPU start at the first instruction. The part of the OS that does this loading and setup is called the loader. "What Is an Operating System?" explains what an OS does.

Why optimization matters

If the line runs only once, saving one multiplication makes no noticeable difference. But many programs have parts that repeat millions of times. If the example line repeats 100 million times, constant folding alone saves 100 million multiplications.

Compilers do many other optimizations too. They remove calculations whose results are never used. They move a calculation that gives the same result on every pass of a loop out of the loop. By stacking dozens of tricks like these, they make the whole program faster.

On the other hand, optimization makes translation take longer. It also makes it harder to stop the program line by line and inspect it. So GCC, a compiler widely used for C and other languages, does almost no optimization unless you ask for it. When building the finished product, people usually turn optimization on with an option such as "-O2."

Compiler vs interpreter vs JIT

There are also ways to run source code without turning it into a machine code file. An "interpreter" is software that reads a program and runs it piece by piece on the spot. Its advantage is that you can write code and try it right away, without waiting for translation. The downside is that it has to work out the meaning again every time the same part runs. So it tends to be slower.

JIT compilation is a method that combines the strengths of both. JIT stands for "just in time" (at the moment it is needed). In a common form, it starts by running the program like an interpreter and finds the parts that run often. It then translates only those parts into machine code while the program is running.

When a compiler, an interpreter and a JIT turn code into machine code Compiler Translate it all Run machine code Before running Interpreter Read and run one piece at a time JIT Read and run Often-used partsas machine code Time
The horizontal direction is time. A compiler finishes translating before the program runs. A JIT starts partway through the run and translates only the often-used parts into machine code. Left out: the earlier step that turns code into an in-between form called bytecode, and how a JIT can undo the parts it translated.

Note that "compiled" and "interpreted" describe the software that runs a language, not the language itself. In practice, many languages combine these methods.

  • Java: The developer compiles the source code in advance into a form called "bytecode" and ships that. Bytecode is not the machine code of any particular CPU. It is a set of instructions for a virtual machine designed for Java. When the program runs, Java's runtime software interprets the bytecode and uses JIT to turn the often-used parts into machine code.
  • JavaScript: V8, the software used in Chrome and other programs, first turns source code into bytecode and runs it with an interpreter. Parts that run often are turned into machine code by an optimizing compiler.
  • Python: CPython, the standard software, also first compiles source code into bytecode and then runs it with an interpreter. Python 3.13, released in 2024, added an experimental JIT, but it is disabled by default.

"Compiler vs Interpreter vs JIT" sums up the three methods in a table.

Errors a compiler can and cannot catch

A compiler finds many errors before the program runs. Examples are grammar errors, using a name that was never set up, and calculations with mismatched types. Some compilers also give warnings about code that is not wrong but looks suspicious.

Other errors only show up when the program runs. For example, a program that divides by a number the user enters cannot calculate if the user enters 0. A missing file that the program expects to read is also found only at run time. Errors like these, which happen while the program is running, are called "runtime errors."

Worse still are errors where the program keeps running, but the results are not what the programmer meant. If you write "subtract" when you meant "add," the compiler simply translates it as written. The only way to find such errors is to run the program and check its results. This is called "testing."

Which errors can be found in advance also depends on the language. In Python, for example, many type mismatches become errors only when that line runs.

A short history of compilers

Until the early 1950s, people wrote programs in machine code or something close to it. In 1951–1952, Grace Hopper created a system called "A-0" for a computer called UNIVAC I. You picked ready-made calculation parts by number, and it joined them together into a machine code program.

A-0 was already called a "compiler" at the time. But what it did was different from a compiler in today's sense. The IEEE, a professional society for electrical and electronics engineering, chose A-0 as a milestone in the history of technology. In 2024 it installed a plaque to mark it. Its description, too, calls A-0 "a set of specifications that worked as a linker and loader."

FORTRAN was built by John Backus and his team at IBM. It is credited with laying the groundwork for how today's compilers divide their work, and for their core methods. It translated source code, written much like math formulas, into the machine code of a computer called the IBM 704. The project started in the summer of 1954. In April 1957, the compiler was delivered to every site that used a 704.

If a compiler's machine code had been much slower than code written by hand, no one would have used it. So the FORTRAN team aimed for nearly hand-written speed and put great effort into optimization. A review paper written in 1982 judged that this goal had been met to a surprising degree.

Hardware has its own kind of "compiler"

A similar idea is used in designing the circuits of semiconductor chips. Today, large circuits are usually written as text, not drawn as diagrams. A language for writing circuits is called a hardware description language. Verilog and VHDL are examples.

Logic synthesis automatically turns a written description of a circuit's behavior into a combination of basic logic circuits, such as AND and OR. Its flow of reading, checking, optimizing and producing output is very similar to a compiler's.

The big difference is the output. It is not a sequence of instructions run in order. It is a network of circuits that all work at the same time. The finished circuit is written into a chip that can be reprogrammed later, or manufactured as a dedicated chip. The first kind is explained in "What Is an FPGA?"

Common misconceptions

Myth: Languages compiled to machine code are always fast

In fact, whether code is compiled is only one of the things that decide speed. If the steps of the calculation itself take a long way around, no amount of optimization will make it fast. A JIT can optimize based on how the program behaves while it runs. So for long-running programs, it can come close to the speed of code translated in advance. Also, NumPy, a collection of math code widely used from Python, does its core calculations in pre-compiled C code. When the same task is compared, compiled code is often faster, which is why people tend to think it is "always fast."

Myth: A compiler understands what the programmer wants to do

In fact, a compiler only translates by fixed rules and knows nothing about the programmer's intent. An error message like "missing ; here" looks helpful because it reports where the rules were broken. Optimization, too, happens only where the language rules say the result will not change. In C, code whose result the rules leave undefined can produce machine code you did not expect. This is called undefined behavior. Code that compiles is not necessarily correct.

Myth: Python is not compiled; it runs each line as written, one by one

In fact, the standard CPython turns the whole file into bytecode before running it, and then runs that with an interpreter. So if there is a grammar error anywhere in the file, you get an error before even the first line runs. Still, by default it does not produce machine code, so calling Python "a language run by an interpreter" is not wrong either. The misconception comes from thinking that "compile" only means "turn into machine code."

By the numbers

ItemNumber / dateNote
A-01951–1952Grace Hopper. For the UNIVAC I
Start of the FORTRAN projectSummer 1954IBM, John Backus's team
FORTRAN compiler deliveredApril 1957To every IBM 704 site
Effort to build FORTRANAbout 18 person-yearsThe work of one person for 18 years. From the 1957 paper
GCC default setting-O0Almost no optimization. Use -O2 or similar for speed
Python's experimental JIT2024 (3.13)Disabled by default in 3.13

Nearly 70 years after FORTRAN, the basic idea of translating in stages has not changed much. What has grown is the number of kinds of optimization, and methods like JIT that translate while the program runs.

Learn more

Read next

Articles in the series “How computers work”

  1. How Do Computers Use Binary? Calculating with 0 and 1
  2. What Is a CPU? What It Does Inside Your Computer and Phone
  3. Why Are GPUs Used for AI? Matrix Math and Memory Explained
  4. What Is an Operating System? What an OS Actually Does
  5. How Does CPU Cache Work? Hit Rate, Lines and Locality
  6. What Is an NPU? The AI Engine in Phones and PCs Explained
  7. What Is an FPGA? A Chip You Can Rewire After It's Made
  8. What Is an ASIC? Custom Chips vs CPUs and FPGAs Explained
  9. How Compilers Work: From Source Code to Machine Code
  10. What Is a Logic Gate? AND, OR, NOT and How Gates Compute
  11. What Is RISC-V? The Open Instruction Set Explained
  12. What Is an Algorithm? Search, Sorting and Big-O Explained
  13. What Is a Database? Tables, SQL and Indexes Explained
  14. What Is Git? Version Control Basics and Git vs GitHub

Questions on this topic: Compilers

Sources

All explainers