Skip to content

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

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