When you write a single line of code like total = price * quantity; and run your program, that short statement quietly travels through an entire pipeline before it ever becomes something the processor can execute. It gets scanned, checked, restructured, verified for meaning, simplified, optimized, and finally rewritten into instructions the hardware understands. None of this happens in one giant leap. Instead, a compiler breaks the job into a series of clearly defined phases, each with its own responsibility, its own input, and its own output.
Understanding these phases is arguably the single most important step in learning Compiler Design, because almost every other topic in the subject, from parsing techniques to code optimization strategies, fits neatly into one of these stages. Once you have a clear mental picture of the pipeline a program travels through, the rest of the subject starts to feel like filling in details rather than learning something completely new each time.
In this tutorial, you will learn what the phases of a compiler are, how each phase transforms the program, the difference between the analysis phase and the synthesis phase, the role of the symbol table and error handler that support every phase, and how these phases work together using a running example. By the end, you will be able to trace a simple statement all the way from source code to target code in your head.
It would technically be possible to build a single, giant program that reads source code and directly spits out machine code without any clear internal structure. In practice, however, no serious compiler is built this way, because a monolithic design would be extremely difficult to develop, test, debug, and extend.
Dividing the compiler into distinct phases brings several practical advantages. Each phase can be designed, implemented, and tested somewhat independently of the others, since it only needs to worry about transforming one specific representation of the program into the next. This separation also makes it easier to reuse parts of a compiler, retarget it to new hardware, or add new source languages without rewriting the entire system from scratch. This same idea of splitting a large problem into smaller, well-defined stages is a recurring theme throughout computer science, and Compiler Design is one of the clearest examples of it in action.
A typical compiler is organized into six main phases. Each phase takes the output of the previous phase as its input, performs a specific transformation or check, and passes its own output forward to the next phase.
| Phase | Input | Output |
|---|---|---|
| Lexical Analysis | Raw source code as a stream of characters. | A stream of tokens representing keywords, identifiers, operators, and literals. |
| Syntax Analysis | Stream of tokens from lexical analysis. | A parse tree or syntax tree representing the grammatical structure of the program. |
| Semantic Analysis | Parse tree from syntax analysis. | An annotated syntax tree checked for meaning-related correctness, such as type consistency. |
| Intermediate Code Generation | Annotated syntax tree from semantic analysis. | A simplified, machine-independent intermediate representation of the program. |
| Code Optimization | Intermediate representation from the previous phase. | An improved, more efficient version of the same intermediate representation. |
| Code Generation | Optimized intermediate representation. | Final target code, typically assembly or machine instructions for a specific processor. |
Let us walk through each of these phases in more detail, using a small running example so the transformations feel concrete rather than abstract.
Lexical analysis is the very first phase a compiler performs, and it is handled by a component usually called the lexical analyzer or scanner. Its job is to read the source program character by character and group those characters into meaningful chunks called tokens. A token might represent a keyword, an identifier, a numeric literal, an operator, or a punctuation symbol.
During this phase, the lexical analyzer also strips out anything that is not meaningful to later phases, such as whitespace and comments, so that the rest of the compiler does not need to worry about formatting details.
Source Code: total = price * quantity; Tokens Produced: IDENTIFIER(total) ASSIGN_OP IDENTIFIER(price) MULT_OP IDENTIFIER(quantity) SEMICOLON
Internally, lexical analyzers are usually built using concepts from automata theory, since the patterns that define valid tokens, such as what counts as a valid identifier or a valid number, can be described using regular expressions and recognized using finite automata.
Once the source program has been broken into tokens, the syntax analyzer, often called the parser, takes over. Its job is to check whether the sequence of tokens follows the grammatical rules of the programming language and to organize them into a tree-like structure called a parse tree or syntax tree.
If the tokens do not follow a valid grammatical pattern, for example, if an operator is missing or a statement is not properly terminated, the syntax analyzer detects this as a syntax error and reports it, usually along with the approximate location in the source code where the problem was found.
Tokens:
IDENTIFIER(total) ASSIGN_OP IDENTIFIER(price) MULT_OP IDENTIFIER(quantity) SEMICOLON
Simplified Syntax Tree:
=
/ \
total *
/ \
price quantity
This tree makes the structure of the statement explicit. It shows that the assignment operator sits at the top, with the variable being assigned on one side and a multiplication expression on the other, which itself has two operands.
Passing syntax analysis only guarantees that a program is structurally valid, not that it actually makes sense. Semantic analysis is the phase responsible for checking meaning-related correctness, using the syntax tree produced in the previous phase along with information stored in the symbol table.
Common checks performed during this phase include verifying that variables are declared before they are used, confirming that operations are performed on compatible data types, and checking that function calls match the expected number and type of arguments. When a check fails, the compiler reports a semantic error, even though the code may have looked perfectly valid from a purely grammatical point of view.
int price; char quantity; total = price * quantity;
In this example, multiplying an integer by a character type might be flagged or automatically adjusted depending on the language rules, since semantic analysis is where such type-related decisions and checks take place.
After a program has been verified for both structure and meaning, the compiler generates an intermediate representation, a simplified form of the program that sits conceptually between the original source language and the final target machine code. This representation is designed to be easy to analyze and transform, while still being general enough that it does not depend on any particular processor.
A commonly used form of intermediate representation is three-address code, where each instruction involves at most three operands, making the structure of computations very explicit.
Three-Address Code: t1 = price * quantity total = t1
Notice how the single source statement has been broken down into two simple steps, each performing exactly one operation. This uniform, simplified structure makes it much easier for the compiler to reason about and improve the program in the next phase.
Code optimization takes the intermediate representation and improves it, aiming to make the final program run faster, use less memory, or consume fewer resources, all without changing what the program actually computes. Optimization can happen at a local level, examining a small block of instructions at a time, or at a more global level, analyzing the flow of an entire function or program.
Some common optimization techniques include eliminating calculations whose results are never used, avoiding recomputation of values that do not change, and simplifying expressions that can be evaluated more efficiently. It is worth emphasizing that optimization must always preserve the original meaning of the program. A compiler is never allowed to change what a program computes in the name of making it faster.
Before Optimization: t1 = price * quantity t2 = t1 + 0 total = t2 After Optimization: t1 = price * quantity total = t1
Here, an unnecessary addition of zero has been removed, since it has no effect on the final result. Real compilers apply many such transformations, often far more sophisticated than this simple example, across the entire intermediate representation.
The final phase of a compiler is code generation, where the optimized intermediate representation is translated into the actual target code, typically assembly language or machine instructions specific to a particular processor. This phase must take into account details that earlier, more abstract phases could safely ignore, such as the specific registers available on the target machine, memory addressing modes, and instruction formats.
Target Code: MOV R1, price MUL R1, quantity MOV total, R1
At this point, the program is finally expressed in a form that can be assembled and executed directly by the target machine, completing the journey that began with a single readable line of source code.
The six phases described above are commonly grouped into two broader stages, which provides a useful high-level way of thinking about compiler structure.
| Aspect | Analysis Phase (Front End) | Synthesis Phase (Back End) |
|---|---|---|
| Phases Included | Lexical analysis, syntax analysis, and semantic analysis. | Intermediate code generation, code optimization, and code generation. |
| Primary Goal | Understand the source program and verify that it is correct. | Use that understanding to construct efficient target code. |
| Dependency | Depends on the rules of the source programming language. | Depends on the architecture of the target machine. |
| Reusability | Can often be reused across different target machines for the same language. | Can often be reused across different source languages for the same target machine. |
This front end and back end separation is one of the most powerful ideas in Compiler Design, since it allows compiler builders to mix and match different front ends and back ends rather than building an entirely new compiler from scratch for every combination of source language and target machine.
Alongside the six main phases, two components operate continuously throughout the entire compilation process rather than belonging to a single phase.
The symbol table is a data structure used to record information about every identifier in the program, including variable names, function names, their types, and their scope. It is created and updated starting from the earliest phases and is consulted repeatedly by later phases, particularly semantic analysis and code generation, whenever information about an identifier is needed.
The error handler is responsible for detecting, reporting, and sometimes recovering from problems encountered at any phase, whether it is an invalid character during lexical analysis, a grammar violation during syntax analysis, or a type mismatch during semantic analysis. A well-designed error handler tries to continue processing after an error where possible, so that a single mistake does not prevent the compiler from reporting other unrelated issues in the same run.
| Mistake | Correct Understanding |
|---|---|
| Assuming all six phases must always run one after another in strict sequence with no overlap. | While the logical order is fixed, real compilers often interleave phases for efficiency, such as generating tokens on demand as the parser requests them. |
| Believing that syntax analysis alone is enough to guarantee a program is correct. | Syntax analysis only checks structure. Semantic analysis is still required to catch meaning-related errors. |
| Thinking optimization is a single step rather than an entire phase with many techniques. | Code optimization includes a wide variety of techniques, applied at different levels, all aimed at improving efficiency without changing program behavior. |
| Overlooking the symbol table and error handler as unimportant side components. | These two components are used throughout nearly every phase and are essential to how a compiler actually functions in practice. |
The phases of a compiler describe the orderly journey a program takes from readable source code to executable target code. Lexical analysis groups characters into tokens, syntax analysis arranges those tokens into a valid grammatical structure, semantic analysis checks that structure for meaning, intermediate code generation produces a simplified representation, code optimization improves that representation, and code generation produces the final target code. Supporting all of this, the symbol table tracks identifier information and the error handler catches problems along the way.
In this tutorial, you learned why compilers are divided into phases, what each of the six phases does with a running example, how those phases group into the analysis and synthesis stages, and the role played by the symbol table and error handler throughout the process. With this pipeline clearly in mind, you are ready to explore the first phase in depth, starting with how lexical analysis actually breaks source code into tokens.