Basic Principles of Compilers
The core ideas that every compiler is built on: what a compiler must do, how it is structured into a front end and back end, and the role of the symbol table.
What is a Compiler?
A compiler is a program that reads source code written in a high-level language (like C, Java, or Go) and translates it into low-level code (like machine code or assembly) that a computer can execute directly.
High-Level Source Code
↓
[ Compiler ]
↓
Machine Code / Assembly
↓
[ CPU runs it ]
The compiler acts as a bridge between the programmer and the hardware. You write in a language that is easy for humans; the compiler produces code that is easy for the machine.
Key Principles Every Compiler Must Follow
1. Correctness (Most Important) The translated program must behave exactly the same as the original source program. A faster but incorrect compiler is worse than useless.
2. Efficiency The generated code should run quickly and use memory wisely. The compiler itself should not take too long to run either.
3. Clear Error Reporting When a programmer makes a mistake, the compiler must say what went wrong, where, and ideally why. A vague error message wastes time.
4. Portability A well-designed compiler separates "understanding the source language" from "producing code for a specific machine." This lets you target multiple hardware platforms from one source language.
Front End and Back End
A compiler is split into two major halves:
Front End — Analysis (understands the source)
- Lexical Analysis: splits code into tokens (words)
- Syntax Analysis: checks grammar (build a parse tree)
- Semantic Analysis: checks meaning (types, scopes)
- Produces: Intermediate Representation (IR)
Back End — Synthesis (produces the target)
- Code Optimization: makes IR faster/smaller
- Code Generation: turns IR into machine instructions
- Reads: Intermediate Representation (IR)
Source Code
↓
[Front End] ← knows source language rules
↓
Intermediate Representation (IR)
↓
[Back End] ← knows target machine rules
↓
Target Code (machine instructions)
Why this split matters: You can swap out the back end to support a new CPU, without touching the front end. You can add a new language by writing only a new front end, reusing the same back end.
The Symbol Table
Throughout compilation, the compiler builds a symbol table — a dictionary of everything it knows about the names in your program: variables, functions, types, and where they live.
| Name | Kind | Type | Scope | Location |
|---|---|---|---|---|
| x | variable | int | global | 0x1000 |
| add | function | int→int | global | 0x2040 |
| result | variable | float | local | stack+8 |
Every phase reads and writes the symbol table. Without it, the compiler could not know whether x is an integer or a string, or whether a function was defined before it was called.
Error Handling
At every phase, mistakes in the source code can be detected:
- Lexical error: invalid character — e.g.,
@where it doesn't belong - Syntax error: grammar violation — e.g., missing
}or; - Semantic error: type mismatch — e.g., adding a string and an integer
A good compiler does not stop at the first error. It recovers and continues scanning so it can report as many errors as possible in one pass, saving the programmer multiple compile-debug cycles.