What is a compiler?

Free preview 5 min to read

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 require is slow (parsing + compiling on every boot)
  • Why bootsnap helps (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)
Lesson 1 of 7 · free

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.

Type to start searching...