What is a compiler?
The translation problem
Computers only understand machine code: raw binary instructions specific to a CPU architecture (x86, ARM, etc.). But humans write code in high-level languages like Ruby, C, or Go. Something must bridge this gap.
A compiler is a program that translates source code written in one language (the source language) into another language (the target language) (typically machine code or an intermediate representation) before the program runs.
flowchart LR
S["Source code<br/>hello.c<br/>written once by the developer"] --> C["Compiler (gcc)<br/>lexer · parser · optimizer · code gen"] --> B["Machine code<br/>hello (binary) 01001010...<br/>runs directly on the CPU, many times"]
The main point: compilation happens once, execution happens many times. This is why compiled programs are fast: the translation cost is paid upfront.
The compilation pipeline
A compiler typically works in phases, each transforming the code further:
flowchart TB
S["Source code"] --> L["1. Lexical analysis<br/>breaks code into tokens"] --> P["2. Parsing<br/>builds an AST from tokens"] --> SA["3. Semantic analysis<br/>checks types, scopes, undeclared variables"] --> O["4. Optimization<br/>dead code elimination, constant folding, inlining"] --> CG["5. Code generation<br/>machine code, bytecode..."] --> B["Executable binary"]
We will explore each phase in detail in later lessons. For now, understand that a compiler is not a single monolithic step: it is a pipeline of transformations.
Compiled languages in practice
Some well-known compiled languages:
| Language | Compiler | Target | Use Case |
|---|---|---|---|
| C | gcc, clang | Machine code | OS kernels, embedded systems |
| C++ | g++, clang++ | Machine code | Games, browsers, databases |
| Go | go build | Machine code | Servers, CLI tools |
| Rust | rustc | Machine code | Systems programming, safety |
| Java | javac | Bytecode (JVM) | Enterprise, Android |
| Swift | swiftc | Machine code | iOS/macOS apps |
Compilation in C: a concrete example
// hello.c
#include <stdio.h>
int main() {
printf("Hello, world!\n");
return 0;
}
# Compile: source code → machine code
gcc -o hello hello.c
# The result is a standalone binary
file hello
# hello: Mach-O 64-bit executable arm64
# Run the binary — no compiler needed at runtime
./hello
# Hello, world!
Notice: after compilation, the binary runs without gcc installed. The compiler's job is done. This is fundamentally different from interpreted languages.
Ahead-of-time (AOT) vs just-in-time (JIT)
Traditional compilers are Ahead-of-Time (AOT): they compile everything before execution:
flowchart LR
A["Compile ALL<br/>slow start"] --> B["Run binary<br/>fast execution"]
But some systems use Just-in-Time (JIT) compilation: they compile code during execution:
flowchart LR
A["Start running<br/>fast start"] --> B["Identify hot code<br/>profile"] --> C["Compile the hot path<br/>optimize"] --> D["Run faster<br/>speed up"]
JIT compilers can sometimes produce faster code than AOT because they have runtime information (which branches are taken, which types are used). We will explore JIT in depth in the final lesson.
Why this matters for Ruby developers
Ruby is often called an "interpreted language," but the reality is more nuanced:
- CRuby (MRI) compiles your code to bytecode, then interprets that bytecode on the YARV virtual machine
- YJIT (Ruby 3.1+) adds a JIT compiler that compiles hot bytecode to machine code at runtime
- TruffleRuby uses the GraalVM JIT for aggressive optimization
- mruby can precompile scripts to bytecode ahead of time (with
mrbc) for embedded systems
flowchart LR
R["your_code.rb"] --> L["Lexer"] --> P["Parser"] --> C["Compiler"] --> B["Bytecode"] --> V["YARV VM"]
V --> J["YJIT"] --> M["Machine code"]
Understanding compilation helps you understand:
- Why Ruby is "slower" than C (interpretation overhead)
- Why YJIT makes Ruby faster (eliminating that overhead for hot paths)
- Why
requireis slow (parsing + compiling on every boot) - Why
bootsnaphelps (caches compiled bytecode)
Cross-compilation
A cross-compiler produces code for a different platform than the one it runs on:
flowchart LR
M["Your Mac (ARM64)<br/>cross-compiler runs here"] -->|binary| S["Linux server (x86_64)<br/>runs here"]
This is common in Go:
# Compile on Mac for Linux
GOOS=linux GOARCH=amd64 go build -o myapp
Ruby gems with native extensions (like pg, nokogiri) contain C code that must be compiled for each platform. Many ship precompiled versions for common platforms; when yours isn't covered, bundle install compiles from source, and that step sometimes fails on a different architecture.
Summary
| Concept | Description |
|---|---|
| Compiler | Translates source code to target code before execution |
| Compilation pipeline | Lexing → Parsing → Semantic Analysis → Optimization → Code Gen |
| AOT compilation | Compile everything before running (C, Go, Rust) |
| JIT compilation | Compile hot code during execution (YJIT, JVM) |
| Machine code | Binary instructions the CPU executes directly |
| Cross-compilation | Compiling on one platform to run on another |
| Bytecode | Intermediate code for a virtual machine (not raw machine code) |
Keep going with lesson 2
What is an interpreter?
Explains tree-walking interpreters, the compiler to interpreter spectrum, REPLs, Ruby's dynamic features and bootsnap caching.
A free account also gives you the quizzes, your progress and notes, and the discussion.