Getting Started: Programs and the Compiler · about 30 min

What a Program Is, and What a Compiler Does

Why this matters

Everything you will write in Rust ends up as a file of numbers that your CPU executes. Knowing what happens between the text you type and the program that runs explains a great deal: why Rust catches some bugs before the program ever starts, why compiling takes a few seconds, why the finished program is fast, and why the compiler's messages are worth reading closely. Rust leans on its compiler more than most languages, so a clear picture of the compiler pays off in every lesson that follows.

What a program is

A CPU understands only a small set of very simple instructions, each one a number: load a value from memory into a register, add two registers, compare, jump to another instruction if the comparison came out a certain way, store a register back to memory. A program, at the lowest level, is a long sequence of such instructions plus the data they work on. That form is called machine code. It is specific to a CPU family: x86-64 machine code does not run on an ARM chip.

Nobody writes machine code by hand any more. We write source code in a language designed for people, and a tool translates it. There are two broad ways to do the translation:

Rust is compiled. The compiler is called rustc.

What rustc does, step by step

When you run rustc main.rs, a lot happens in a couple of seconds:

 main.rs (text)
     |
     |  1. parse: characters -> tokens -> syntax tree
     v
 syntax tree ---- 2. expand macros (println! becomes ordinary code)
     |
     |  3. resolve names, check types, check ownership and borrowing
     v
 checked program -- errors stop here, with a message, and no executable is written
     |
     |  4. lower to an intermediate form, optimize (LLVM)
     v
 machine code ----- 5. link with the standard library and the C runtime
     |
     v
 main (executable file on disk)

Step 3 is where Rust is unusual. Besides checking that types line up (you cannot add a number to a piece of text), rustc checks rules about who owns each piece of memory and who may change it. Whole families of bugs that crash C programs or corrupt their data at run time are, in Rust, compile errors. The price is that the compiler refuses programs more often, and learning Rust is partly learning to read its refusals. The upside is that those refusals are some of the best error messages of any compiler.

Step 5, linking, glues your code together with the parts of the standard library you used (the code that actually writes text to the terminal, for example) and produces a file the operating system knows how to load. On Linux that file format is called ELF.

Your first program

Here is the smallest useful Rust program:

fn main() {
    println!("Hello, world!");
}

Piece by piece:

Compile and run it in a terminal:

$ rustc main.rs
$ ./main
Hello, world!

rustc main.rs writes an executable called main next to the source. ./main asks the shell to run the file named main in the current directory. The exercises in this course build your code with a few extra options, rustc --edition 2021 -g -o prog main.rs: --edition 2021 selects the current version of the language rules, -g keeps debugging information, and -o prog names the output file prog.

Looking at the result

The executable is an ordinary file, and you can inspect it:

$ file prog
prog: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, ...
$ ls -l prog
-rwxr-xr-x 1 learner learner 11390376 Nov  1 14:06 prog

Eleven megabytes for one line of output looks absurd. Almost all of it is debugging information for the standard library; the machine code for your main is a few dozen bytes. A release build with symbols stripped is a few hundred kilobytes. You do not need to care about size now, but it is a good reminder that the file contains far more than your own code.

Notice what is not needed to run prog: not main.rs, not rustc. You could copy prog to another Linux x86-64 machine without Rust installed and it would run. That is the practical meaning of "compiled ahead of time".

Statements run in order

A function body is a sequence of statements, executed top to bottom:

fn main() {
    println!("one");
    println!("two");
    println!("three");
}
one
two
three

The order on the page is the order of execution. This sounds too obvious to say, but the programs in later lessons will jump around with loops, function calls and conditions, and "which line runs next?" is the question you will ask most while reading code.

Why println! is a macro

Functions in Rust take a fixed number of arguments with fixed types. println! takes a format string followed by any number of values of any printable type, and it checks at compile time that the string and the values match:

fn main() {
    let apples = 3;
    println!("I have {} apples", apples);
}
I have 3 apples

The compiler expands the macro into ordinary code that formats apples and writes the result. If you wrote println!("I have {} apples") with no value for the {}, the program would not compile, because the check happens during expansion. You will see more of this in the lesson on formatting; for now, read name!(...) as "a macro call: code that writes code".

Common mistakes

Forgetting a semicolon. This is the error you will meet most often at first:

fn main() {
    println!("Hello, world!")
    println!("Bye");
}
error: expected `;`, found `println`
 --> main.rs:2:30
  |
2 |     println!("Hello, world!")
  |                              ^ help: add `;` here
3 |     println!("Bye");
  |     ------- unexpected token

The message names the file, line and column (main.rs:2:30), draws the line, and puts a caret exactly where the semicolon should go. Most rustc messages have this shape; the next lessons look at them in detail.

Misspelling a name. Write printline! instead of println! and rustc tells you so, and even suggests the right name:

error: cannot find macro `printline` in this scope
 --> main.rs:2:5
  |
2 |     printline!("Hello");
  |     ^^^^^^^^^ help: a macro with a similar name exists: `println`

Leaving out the !. println("Hi"); asks for a function called println, which does not exist, so you get cannot find function println in this scope. The fix is the !.

Running the old executable. If compiling fails, the old prog (from the last successful build) is still on disk. Running it shows the old behavior and is very confusing. Always check that the build succeeded before you trust what the program prints.

Recap

Going further: after a successful build, try objdump -d prog | less in the sandbox shell and search for main to see real machine code (it will look like noise for now, and that is the point). rustc --help lists the compiler's options.

Exercises

Exercise 1 · quiz

What does rustc produce?

You run rustc main.rs on a correct program. What is the main thing rustc produces?

Answer
  • An executable file of machine code that the operating system can load and run

rustc is a compiler: it translates the whole source file ahead of time into machine instructions for your CPU and writes them into an executable (named main by default, or whatever -o says). Running the program is a separate step, ./main, and from then on the source file is not needed at all. You could delete main.rs and the program would still run.

Exercise 2 · write code

Hello, world

Write a complete Rust program that prints exactly this one line:

Hello, world!

Remember that every Rust program starts running at a function called main.

File: main.rs

Starting point

// Write the main function here.

Example

expected output

Hello, world!
Hint 1

The entry point is written fn main() { ... }.

Hint 2

Inside the braces, println!("..."); prints a line. Do not forget the ! and the ;.

Reference solution
fn main() {
    println!("Hello, world!");
}

Exercise 3 · write code

Three lines, in order

Print these three lines, exactly, in this order:

Rust compiles ahead of time.
The CPU runs machine code.
The source is not needed at run time.

File: main.rs

Starting point

fn main() {
    println!("Rust compiles ahead of time.");
}

Example

expected output

Rust compiles ahead of time.
The CPU runs machine code.
The source is not needed at run time.
Hint 1

One println! per line. Statements run top to bottom.

Reference solution
fn main() {
    println!("Rust compiles ahead of time.");
    println!("The CPU runs machine code.");
    println!("The source is not needed at run time.");
}

Exercise 4 · write code

Fix the program

This program does not compile. Build it (press Run or Check), read the error message carefully, and fix the program so that it prints:

first
second

File: main.rs

Starting point

fn main() {
    println!("first")
    println!("second");
}

Example

expected output

first
second
Hint 1

The error says expected ; and points just after the first println!(...).

Hint 2

Statements inside a function body end with a semicolon.

Reference solution
fn main() {
    println!("first");
    println!("second");
}

Exercise 5 · quiz

What is the ! for?

In println!("Hi"), what does the exclamation mark tell you?

Answer
  • println is a macro, expanded into ordinary Rust code at compile time

A name followed by ! is a macro call. Macros run inside the compiler and expand into regular code before it is compiled. println! is a macro because it needs to do things a plain function cannot: accept any number of arguments, check at compile time that the format string matches them, and generate code specialized for their types.