Types of Compilers
Not all compilers work the same way. Learn the different types — single-pass, multi-pass, cross, JIT, and transpilers — and when each one is used.
Why Are There Different Types?
Different problems call for different compiler strategies. A compiler for an embedded microcontroller has very different needs from one that runs inside a web browser.
Compilers are classified by:
- How many passes they make over the source code
- What target they produce code for
- When they translate the code (ahead of time vs at runtime)
- What level of source code they accept
Single-Pass Compiler
A single-pass compiler reads the source code exactly once from top to bottom and produces the output in one sweep.
- Fast and uses little memory
- Limited: it cannot look ahead or back, so some optimizations are impossible
- Requires the programmer to declare things before using them
Example: Early C compilers, Pascal compilers
Source Code → [Read once, top to bottom] → Machine Code
Think of it like reading a recipe and cooking at the same time — you cannot go back to re-read step 2 after you have already moved to step 5.
Multi-Pass Compiler
A multi-pass compiler reads the source code several times, each pass doing a different job.
- More powerful — can perform deeper analysis and better optimization
- Uses more memory (stores intermediate results between passes)
- Most modern compilers are multi-pass
Pass 1 → Build symbol table
Pass 2 → Type checking and semantic analysis
Pass 3 → Generate intermediate code
Pass 4 → Optimize
Pass 5 → Generate machine code
Example: GCC, LLVM (Clang), Java compiler (javac)
Cross Compiler
A cross compiler runs on one machine but produces code for a different machine (different CPU or OS).
- Used when developing software for devices that cannot compile their own code (e.g., tiny microcontrollers, game consoles, embedded systems)
- The host machine (where you develop) is different from the target machine (where the code runs)
Developer's PC (x86)
↓
[Cross Compiler]
↓
Code for ARM microcontroller
Example: Arm-none-eabi-gcc (compiles C for ARM from a PC), Android NDK toolchain
Just-In-Time (JIT) Compiler
A JIT compiler compiles code at runtime, right before it is executed — combining the benefits of both compilers and interpreters.
- Starts executing quickly like an interpreter
- Compiles hot (frequently-run) code paths into fast machine code
- Gets faster the longer the program runs (warm-up period)
Bytecode / IR
↓
[JIT detects hot code]
↓
Compile to native machine code on the fly
↓
Run the native code directly
Example: JVM HotSpot (Java), V8 (JavaScript/Node.js), .NET CLR (C#)
Source-to-Source Compiler (Transpiler)
A transpiler (source-to-source compiler) translates source code in one high-level language into source code in another high-level language.
- Output is still readable, human-editable code
- Often used to add features to a language or target an older standard
| Input Language | Transpiler | Output Language |
|---|---|---|
| TypeScript | tsc | JavaScript |
| CoffeeScript | coffee | JavaScript |
| Sass | Sass compiler | CSS |
| C++ (modern) | Emscripten | WebAssembly / JS |
Example: TypeScript compiler (tsc), Babel (modern JS → older JS)
Incremental Compiler
An incremental compiler only recompiles the parts of a program that changed since the last build.
- Saves a lot of time in large projects with many files
- Tracks dependencies between files
- Recompiles only affected modules
Example: Most IDE build systems (VS Code tasks, Gradle for Java), TypeScript in watch mode (tsc --watch)
Without incremental compilation, rebuilding a 1-million-line project after changing one line would recompile everything — which could take minutes.