What a compiler does before your program runs¶
Level: 101 · newcomer
One line: A compiler is not a translator working line by line — it is a program that reads your source, runs some of it, rejects what it cannot prove, and emits something the machine can execute; and the line between what it does and what your program does is the one worth being able to see.
Three things happen to a .rs file, in this order, and every error you will ever get comes from one of them:
| Stage | What it takes in | What it produces | The errors it can raise |
|---|---|---|---|
| Source | your text | tokens, then a syntax tree | expected one of ..., unclosed brace, bad token |
| Compilation | that tree | machine code, one object file per unit | type errors, borrow errors, E0308, overflow in a const |
| Linking | object files + libraries | one executable | undefined symbol, duplicate symbol, missing library |
The middle row is the one people mean by "the compiler", and it is where Rust differs from every language you already know: type checking, ownership, lifetimes, exhaustive match. It is also where the compiler executes your code.
The compiler runs your program's arithmetic¶
A const fn may be called during compilation. An array's length must be known before the binary exists, so using one as a length forces the compiler to run it:
const fn triangular(n: u64) -> u64 {
let mut sum = 0;
let mut i = 1;
while i <= n { sum += i; i += 1; }
sum
}
const SLOTS: usize = triangular(10) as usize; // 55, computed during the build
let counters = [0u8; SLOTS]; // only compiles because it did
Verified output of what_a_compiler_does.rs — regenerated by tools/run_examples.py, never hand-typed.
compile time: SLOTS = 55
compile time: counters.len() = 55
run time: triangular(n) = 55
both agree: true
The loop in triangular ran twice: once inside the compiler, ten additions on the compiler's own stack, producing the number 55 baked into the binary — and once at run time, in the compiled program, when black_box hid the argument so the compiler could not do the work in advance.
The boundary, seen from both sides¶
The clearest way to feel the line is to cross it in each direction. Neither of these compiles.
A run-time value where a compile-time one is required. n is a let binding: it has a value only once the program is running, and the array length is needed before that.
error[E0435]: attempt to use a non-constant value in a constant
--> e1.rs:3:26
|
3 | let counters = [0u8; n];
| ^ non-constant value
|
help: consider using `const` instead of `let`
A run-time failure that happens during the build. u64::MAX + 1 would panic at run time. In a const, the addition is performed by the compiler, so the panic happens to it — and a compiler that has crashed on your arithmetic reports it as a compile error instead:
error[E0080]: attempt to compute `u64::MAX + 1_u64`, which would overflow
--> e2.rs:1:18
|
1 | const BIG: u64 = u64::MAX + 1;
| ^^^^^^^^^^^^ evaluation of `BIG` failed here
That second one is the useful mental model: const evaluation is your program running early, with the compiler as its runtime. Everything that would be a panic becomes a build failure, which is a straight upgrade — nobody's laptop is on fire at 3am over an error found in cargo build.
If you are coming from another language¶
Python. Python compiles too — import produces bytecode and caches it in __pycache__/*.pyc — but it compiles syntax, not meaning. A misspelled attribute, a str where an int was wanted, a call with three arguments to a two-argument function: all of that is discovered when the line executes, if it ever does. The nearest thing to const evaluation is a module-level expression, which runs at import time rather than at compile time, and can therefore do anything at all — including fail on your user's machine. Rust's const evaluation is deliberately much smaller than a language: no filesystem, no clock, no allocation surprises, because its answer has to be baked into a binary that ships.
ABAP. The two-stage shape is familiar: a program is generated into load form, and syntax errors surface at activation rather than at run. What is new is how much moves to the earlier stage. sy-subrc is a value you can forget to read; Result is a value the compiler will not let you ignore. A MOVE between incompatible types is a short dump on a user's screen; the equivalent in Rust is an E0308 on your own screen, before anything is transported.
C and C++. All of this is already familiar, including the linker, and constexpr is const fn's direct counterpart — Rust's version is simply the default direction of travel rather than an opt-in for the performance-minded. The genuine difference is at the end of compilation: C++ hands the linker a set of object files and trusts that the declarations in the headers matched the definitions; Rust checks that within a crate, and only loses the guarantee at an extern block, which is exactly why those need unsafe.
See also¶
- What the optimizer does — the other way to arrive at 55: not by running the loop early, but by deleting it
- The linker — the third stage, the one that is not rustc
- Compile times — the same phases, measured, and the three knobs that reach them
- Running a scratch program — invoking all of this by hand, one file at a time
- What a warning is asking — the diagnostics that are not errors, and why the compiler bothers
Po polsku¶
W polskiej szkole i na pierwszym roku studiów języki dzieli się na kompilowane i interpretowane: „kompilator tłumaczy cały program przed uruchomieniem, interpreter wykonuje go linia po linii”. Ta strona pokazuje miejsce, w którym ten podział przestaje wystarczać — rustc niczego nie tłumaczy linia po linii, za to wykonuje część twojego programu, zanim program w ogóle powstanie. Rozmiar tablicy musi być znany przed zbudowaniem pliku wykonywalnego, więc const SLOTS: usize = triangular(10) as usize; zmusza kompilator do przejścia całej pętli while — dziesięć dodawań na stosie kompilatora — i wpisania do binarki gotowej liczby 55. W wydruku powyżej ta sama pętla przebiega dwa razy: raz w czasie kompilacji i raz w czasie wykonania, gdy black_box ukrywa argument na tyle, że policzenie z góry przestaje być możliwe.
Granicę najlepiej widać, gdy przekroczy się ją w obie strony, bo każdy kierunek ma własny błąd. Zmienna z let ma wartość dopiero w czasie wykonania, więc [0u8; n] kończy się jako E0435: attempt to use a non-constant value in a constant. W drugą stronę: u64::MAX + 1 w zwykłym kodzie byłoby paniką w czasie działania, ale wewnątrz stałej dodawanie wykonuje kompilator — więc panikuje on, a ty dostajesz E0080 i zdanie which would overflow przy budowaniu. Stąd model wart zapamiętania: obliczanie stałych to twój program uruchomiony wcześniej, a jego środowiskiem uruchomieniowym jest kompilator. Zamiana jest bardzo korzystna — błąd, który normalnie budzi kogoś o trzeciej w nocy, przenosi się do cargo build. Warto tylko pamiętać, że ten „wcześniejszy program” jest celowo ubogi: bez plików, bez zegara, bez wejścia-wyjścia, bo jego wynik zostaje na stałe wpisany do binarki, która trafi do ludzi.
Szukaj po polsku: czas kompilacji a czas wykonania · obliczenia w czasie kompilacji · kompilator a interpreter · rust const fn · rust E0435 non-constant value